@kensio/skills 1.13.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.
@@ -0,0 +1,356 @@
1
+ # Where the rules came from
2
+
3
+ The measured patterns in `SKILL.md` were selected by comparing human-written and LLM-written
4
+ technical documentation and keeping only what separated them. This file records the method, the
5
+ numbers, and the candidates that were tested and dropped, so the thresholds can be argued with and
6
+ the study can be re-run.
7
+
8
+ ## The corpora
9
+
10
+ **Human exemplars.** 65,767 words across 15 documents from four independent sources. Four Django
11
+ documentation topics (`db/queries`, `http/urls`, `testing/overview`, `cache`), Effective Go, six
12
+ chapters of the Rust Book, and four Python documentation pages. All four are long-lived technical
13
+ documentation for developer audiences, written and revised by many hands, and all four predate
14
+ widespread LLM drafting.
15
+
16
+ **LLM corpora.** 192,430 words across 60 documents from three independent sources. The `docs/` tree
17
+ of a TypeScript library, the architecture READMEs under its `src/` tree, and the skills in a
18
+ separate repository. Different projects, different genres, one author model.
19
+
20
+ It matters that the first LLM corpus was written under a style guide already. That guide banned em
21
+ dashes, marketing language, the rule of three, preambles and long sentences. The guide was followed.
22
+ Em dashes fell to 0.33 per 1000 words and promotional vocabulary almost disappeared. The prose still
23
+ read as machine-written. That is the observation that started this.
24
+
25
+ Code blocks, inline code, headings, tables, link targets and URLs were stripped from both before
26
+ counting. Hard-wrapped lines were rejoined into paragraphs first, so sentence boundaries survive.
27
+
28
+ **The 2026 re-measurement.** `contrastive-coda` and the heading finding below come from a later run
29
+ against a freshly fetched corpus, because the original working files were never committed. That run
30
+ used 19 human documents (85,557 words) from the same four sources, and 13 LLM documents (15,022
31
+ words) from this repository. It reproduced the published `contrastive-def` rate to two decimal
32
+ places (1.05 against 1.06) and put `significance-tail` at 0.60 against the published 0.79, close
33
+ enough on a different document selection to treat the two runs as comparable. The LLM side of that
34
+ run is the weaker half. Every document in it had already been rewritten under the rules in
35
+ `SKILL.md`, so its rates show what survives the style guide. An unguided draft would score higher.
36
+
37
+ ## Patterns kept
38
+
39
+ | Pattern | Human /1k | LLM /1k | Ratio |
40
+ | ----------------- | --------- | ------- | ----- |
41
+ | significance-tail | 0.79 | 6.41 | 8.1× |
42
+ | contrastive-coda | 0.30 | 2.53 | 8.3× |
43
+ | contrastive-def | 1.06 | 6.62 | 6.2× |
44
+ | negation-frame | 2.04 | 10.50 | 5.2× |
45
+ | appositive-tail | 0.59 | 3.15 | 5.3× |
46
+ | colon-explainer | 2.05 | 4.74 | 2.3× |
47
+
48
+ `contrastive-coda` is the `X, not Y` form of contrastive definition, and it was missed for four
49
+ releases. The rule in `SKILL.md` always described both forms, and the regex only ever matched
50
+ `rather than` and `instead of`. Measured on prose that had already been rewritten to satisfy that
51
+ regex, the coda still ran at 8.3 times the human rate. That is the sharpest separation in the study.
52
+ The construction was moving to the undetected form, in the same way that banning the em dash moved
53
+ it onto colons.
54
+
55
+ Only the `not` form is detected. The `, no ...` variant was tested and dropped, because both of its
56
+ human-corpus hits were ordinary clauses (", no problem." and ", no result is displayed before the
57
+ next interactive prompt.") and it added 4 LLM hits against 2 human false positives.
58
+
59
+ ## How the thresholds were set
60
+
61
+ Rates are noisy per file, so thresholds come from the distribution and never from the aggregate. For
62
+ each pattern, `fail` is the lowest value that flags **none** of the 15 human documents, and `warn`
63
+ is the 90th percentile of those documents. Banned marks are the exception, described below.
64
+
65
+ | Pattern | Baseline | Warn (human p90) | Fail (zero human hits) | LLM files caught |
66
+ | ----------------- | -------- | ---------------- | ---------------------- | ---------------- |
67
+ | significance-tail | 0.79 | 1.7 | 2.3 | 93% |
68
+ | contrastive-coda | 0.30 | 0.85 | 1.6 | 54% |
69
+ | contrastive-def | 1.06 | 2.3 | 4.0 | 81% |
70
+ | negation-frame | 2.04 | 4.6 | 5.0 | 86% |
71
+ | appositive-tail | 0.59 | 1.1 | 1.8 | 81% |
72
+ | colon-explainer | 2.05 | 2.0 | 3.0 | 77% |
73
+
74
+ The `contrastive-coda` row follows the same rule as the others. Its human maximum is 1.51 per 1000
75
+ words (a Python tutorial page on exceptions) and its 90th percentile is 0.83, so `fail` sits at 1.6
76
+ and `warn` at 0.85. At 1.6 it flags 0 of the 19 human documents and 7 of the 13 LLM ones. That 54%
77
+ is the lowest catch rate of the six, and it is measured against already-rewritten prose, so treat it
78
+ as a floor.
79
+
80
+ At the file level, where a document fails if any single pattern fails, the measured patterns at
81
+ their zero-false-positive settings flag **0% of the human documents and 100% of the LLM documents**.
82
+
83
+ Two later decisions trade some of that away deliberately, and both are house style rather than
84
+ evidence:
85
+
86
+ | Configuration | Human documents flagged | LLM documents flagged |
87
+ | ----------------------------------------------- | ----------------------- | --------------------- |
88
+ | Measured patterns, zero-false-positive settings | 0 of 15 | 100% |
89
+ | With colon-explainer tightened to 3.0 | 3 of 15 | 94% |
90
+ | With the em dash and semicolon bans | 15 of 15 | 100% |
91
+
92
+ The bans flag every human document by construction, because Django, the Rust Book and the Python
93
+ docs all use em dashes and semicolons freely. A run with `--no-bans` drops them and re-checks the
94
+ measured separation, which is how the numbers above stay verifiable.
95
+
96
+ The first calibration used only Django and Effective Go, and set `fail` at three times the mean.
97
+ Once the human corpus was widened that flagged 3 of the 15 human documents, including a Rust Book
98
+ chapter whose subject is contrasting two representations. A style checker that fires on the Rust
99
+ Book is wrong, so the thresholds moved to the rule above.
100
+
101
+ Colon-explainer is the one place the zero-false-positive rule was overridden deliberately. At a
102
+ threshold no human document trips (6.6) it caught only 27% of LLM documents. It is set to 3.0
103
+ instead, as a house decision to suppress the construction. That costs 3 of the 15 human documents,
104
+ all of them Python pages, which lean on the "term, then explanation" form. The cost is known and
105
+ accepted.
106
+
107
+ ## Banned marks
108
+
109
+ Three marks are house rules with no measured threshold behind them. Any occurrence in prose fails.
110
+
111
+ | Mark | Human /1k | LLM /1k, unbanned | Note |
112
+ | ------------------ | --------- | ----------------- | ------------------------------------------- |
113
+ | Em dash | 0.24 | 1.93 | 8× the human rate where no ban was in force |
114
+ | Semicolon | 2.70 | 1.04 | The human corpus uses these 2.6× more |
115
+ | Mid-sentence colon | 1.49 | 4.00 | Banned by house decision, see below |
116
+
117
+ The em dash number corrects an earlier reading. Measured only against the corpus that already banned
118
+ em dashes, the mark looked innocent at 0.33 per 1000 words. Measured against LLM prose written
119
+ without that ban, it runs at 1.93 against a human 0.24. The first measurement was recording
120
+ compliance with the ban and never the underlying habit. Banning it is justified.
121
+
122
+ Semicolons are the opposite case. The human corpus uses them more than twice as often as any LLM
123
+ corpus does. The ban removes almost nothing, and moves the prose slightly toward the machine end of
124
+ the range. It is in place as a house consistency rule and never as a tic detector.
125
+
126
+ Colons are now banned outright, and that is a house decision taken against the evidence rather than
127
+ from it. At zero tolerance every one of the 15 human documents fails, because a mid-sentence colon
128
+ is ordinary English. The reasoning is the same as for the em dash. The construction reads as
129
+ machine-written whoever wrote it, and prose costs little by doing without it. Anyone recalibrating
130
+ this study should treat the colon threshold as a preference and the other four as measurements.
131
+
132
+ A colon ending a line, introducing a list, a code block or the next paragraph, is exempt and stays
133
+ exempt. The human corpus uses that form 4.56 per 1000 words against the LLM corpus's 1.99, and
134
+ documentation cannot be written without it. `toProse` closes a block that ends in a colon so that
135
+ joining paragraphs cannot manufacture a mid-sentence one that nobody wrote.
136
+
137
+ ## Headings
138
+
139
+ Headings are stripped before counting, on the grounds that they are labels rather than prose. That
140
+ left them unscored for four releases, and it turned out to be where negation framing collects.
141
+
142
+ | Corpus | Headings | Negation-framed | Share |
143
+ | ------ | -------- | --------------- | -------- |
144
+ | Human | 437 | 2 | 0.5% |
145
+ | LLM | 128 | 8 | **6.3%** |
146
+
147
+ Half of the LLM hits are the `X, not Y` coda in a heading ("Match service errors by name, not
148
+ instanceof"), so the two findings in this section are one habit surfacing in two places.
149
+
150
+ The two human hits are Django headings documenting a genuine prohibition ("When QuerySets are not
151
+ cached" and "Field name hiding is not permitted"), and a heading like that is doing its job. A hard
152
+ failure would flag them, so `heading-frame` reports every occurrence and never fails a file. It is
153
+ the one check that lists all of its hits, because there are only ever a handful and each one is a
154
+ line to go and fix.
155
+
156
+ ## Where the construction goes next
157
+
158
+ Banning one mark moves the construction to the next available one. That is what happened when em
159
+ dashes were banned and colons absorbed the traffic. With all three marks closed, the remaining exits
160
+ worth watching are bracketed asides and comma splices.
161
+
162
+ | Mark | Human /1k | LLM /1k |
163
+ | ------------------- | --------- | ------- |
164
+ | Parenthetical aside | 6.19 | 0.15 |
165
+
166
+ That gap is the largest in the whole study. The human corpus uses bracketed asides 40 times more
167
+ often than the LLM corpus does. Brackets are not a tic to watch for. They are the missing habit, and
168
+ the natural home for a remark that no longer has a dash or a colon to hang from.
169
+
170
+ A pattern needs at least three occurrences in a file of at least 200 words before it is flagged. A
171
+ rate computed from a single hit says more about the length of the page than about the prose.
172
+
173
+ ## Does it generalise
174
+
175
+ The first version of this study used one human source pair and one LLM corpus, which is enough to
176
+ find a pattern and not enough to trust it. Both sides were then widened to independent sources the
177
+ thresholds had never seen.
178
+
179
+ | Corpus | Words | Files over a fail threshold |
180
+ | ---------------------------------------------------- | ------- | --------------------------- |
181
+ | Django docs, human | 17,244 | 0 of 4 |
182
+ | Effective Go, human | 12,623 | 0 of 1 |
183
+ | Rust Book, human, unseen | 15,430 | 0 of 6 |
184
+ | Python docs, human, unseen | 20,470 | 0 of 4 |
185
+ | Library `docs/` tree, LLM | 108,000 | 29 of 31 |
186
+ | Architecture READMEs, LLM, different genre | 78,000 | 23 of 23 |
187
+ | Skills in another repository, LLM, different project | 6,200 | 5 of 6 |
188
+
189
+ Every pattern held its direction and rough magnitude across all of them. The rules are not an
190
+ artefact of one project's house style.
191
+
192
+ ## Patterns tested and dropped
193
+
194
+ Each of these is a common style-guide rule. None of them separated the corpora.
195
+
196
+ | Candidate | Human | LLM | Verdict |
197
+ | ------------------------------------------------------------------------------------- | ---------- | ---------- | ------------------------ |
198
+ | Mean sentence length | 19.8 words | 20.9 words | 1.06×, no signal |
199
+ | Sentences over 32 words | 11.7% | 10.1% | LLM writes fewer |
200
+ | AI vocabulary (`delve`, `robust`, `seamless`, `powerful`, `crucial`, `leverage`, ...) | 0.80 /1k | 0.08 /1k | LLM scores 10× lower |
201
+ | Gerund-led sentences | 2.36 /1k | 2.71 /1k | 1.15×, no signal |
202
+ | Rule of three, prose only (code spans excluded) | 0.63 /1k | 0.34 /1k | LLM writes half as many |
203
+ | Trailing participles (`, leaving X`, `, making Y`) | 0.30 /1k | 0.14 /1k | LLM writes half as many |
204
+ | Verbless list fragment (noun phrases, no main verb) | 2.52 /1k | 2.53 /1k | 1.00×, no signal |
205
+ | Enumeration, four or more comma-separated items | 3.71 /1k | 4.46 /1k | 1.2×, under the bar |
206
+ | Enumeration, five or more | 1.22 /1k | 1.66 /1k | 1.4×, under the bar |
207
+ | `, no ...` coda (as against `, not ...`) | 0.02 /1k | 0.27 /1k | 2 human hits, both wrong |
208
+
209
+ The vocabulary result is the one worth dwelling on. Django and Effective Go use the words on every
210
+ AI-detector word list ten times more often than the Claude-written corpus does, because those words
211
+ are ordinary English and the corpus had been told to avoid them. A word list is measuring compliance
212
+ with a word list.
213
+
214
+ Sentence length is the same story from the other side. "Break up long sentences" was in the style
215
+ guide, the corpus obeyed it, and the result was the same clause count packed into shorter sentences
216
+ joined by colons.
217
+
218
+ The verbless list fragment is the most surprising of the drops, and it was tested because a reader
219
+ of this repository flagged it as the thing that grated most. It measures at 1.00×. Django and the
220
+ Rust Book build sentences out of bare noun phrases exactly as often as the LLM corpus does. The
221
+ dependency-parser study had already found this from the other direction, with the `appos` relation
222
+ at 0.79× in favour of the human corpus. What makes a pile of them grate is repetition inside one
223
+ document, and every measure in this study is a rate per 1000 words, and that is blind to whether six
224
+ hits are spread through a page or stacked in one paragraph. A measure of within-document shape
225
+ repetition would be the honest way to catch it, and none of the shipped rules is one.
226
+
227
+ ## The dependency-parser study
228
+
229
+ Regexes cannot see grammar, so both corpora were re-measured with a spaCy dependency parse
230
+ (`en_core_web_sm`) to test whether syntactic features detect the tics more accurately.
231
+
232
+ | Parser feature | Human /1k | LLM /1k | Ratio |
233
+ | ----------------------------------------- | --------- | ------- | ----- |
234
+ | Coordination arity 3+ (rule of three) | 0.78 | 1.89 | 2.4× |
235
+ | `parataxis` relation | 0.49 | 0.07 | 0.14× |
236
+ | Trailing subordinate clause | 20.41 | 25.48 | 1.25× |
237
+ | Trailing subordinate clause after a comma | 4.28 | 9.49 | 2.2× |
238
+ | `appos` relation | 8.62 | 6.79 | 0.79× |
239
+ | Clauses per sentence | 1.42 | 1.37 | 0.96× |
240
+
241
+ Then the two features that separated the corpora were re-tested as plain regexes, to see what the
242
+ parse was buying:
243
+
244
+ | Feature | Parse ratio | Regex ratio |
245
+ | -------------------- | ----------- | ---------------------------------------------- |
246
+ | Comma-plus-`so` tail | 2.2× | **8.2×** |
247
+ | Rule of three | 2.4× | 2.6×, then 0.54× once code lists were excluded |
248
+
249
+ The regex wins, and the reason generalises. A parser recognises a whole grammatical class, and that
250
+ class contains all the legitimate uses along with the tic. Trailing subordinate clauses are ordinary
251
+ English at 1.25×. The tic is one narrow collocation inside that class, and grammatical generality
252
+ dilutes it. Adding a parser made detection worse.
253
+
254
+ The parse earned its cost as a research instrument. It falsified four candidate rules that sound
255
+ right, including two that had been written into a style guide. Use it to find and kill candidates,
256
+ then ship regexes.
257
+
258
+ Two traps it exposed, both worth repeating:
259
+
260
+ - **Dialect masquerading as a tic.** "X, Y and Z" without the Oxford comma separates the corpora at
261
+ 4.7×. The exemplars are American and take the Oxford comma. The LLM corpus was written in British
262
+ English. That measurement detects nationality.
263
+ - **Markup masquerading as prose.** Rule of three separates at 2.6× until code spans are excluded,
264
+ at which point it inverts to 0.54×. The whole signal was lists of API names.
265
+
266
+ ## The Pangram study
267
+
268
+ The patterns were selected, calibrated and enforced using measurements that all came from the same
269
+ hand. Pangram is a commercial AI-text detector, and it had no part in defining any rule, so it was
270
+ used as an outside judge of whether the rewriting achieves anything.
271
+
272
+ **Instrument check.** Four human documents (Django, two Rust Book chapters, a Python tutorial page)
273
+ scored `fraction_ai` of 0.00 and came back "Human Written". No false positives on this genre, so its
274
+ verdicts here can be relied on.
275
+
276
+ **Treatment.** Six documents in this repository were scored before and after being rewritten to
277
+ satisfy every rule above.
278
+
279
+ | Document | Before | After |
280
+ | ----------------------------- | -------- | -------- |
281
+ | isolated-testing-style SKILL | 0.87 | 0.48 |
282
+ | yulin-aws-simulation SKILL | 1.00 | 0.82 |
283
+ | root README | 0.65 | 0.46 |
284
+ | isolated-testing-style README | 1.00 | 1.00 |
285
+ | part-factory SKILL | 1.00 | 1.00 |
286
+ | yulin-aws-simulation README | 0.68 | 1.00 |
287
+ | **Mean** | **0.87** | **0.79** |
288
+
289
+ All six were still identified as AI after the rewrite. One scored worse. Edit volume failed to
290
+ predict the change. The document with the most edits improved by 0.18, and the one with the fewest
291
+ stayed where it was.
292
+
293
+ The conclusion is that style and provenance are different signals. The patterns are real differences
294
+ between human and LLM technical writing, and a classifier keys on something else. `SKILL.md` says
295
+ so, under "Limits".
296
+
297
+ One reassuring result. No window in any group was flagged `is_humanized`. The rewriting is not
298
+ producing evasion artefacts, largely because it moves the score so little.
299
+
300
+ ## What the Pangram study did find
301
+
302
+ Pangram scores windows of roughly 300 words, which gave 109 windows across the four groups. Features
303
+ were computed per window and correlated with the window score.
304
+
305
+ | Feature | Human | LLM | r with score |
306
+ | ----------------------------- | ----- | ----- | ------------ |
307
+ | Distinct words per 100 | 0.627 | 0.699 | **0.51** |
308
+ | Parenthetical asides per 1000 | 5.90 | 0.99 | -0.26 |
309
+ | The five patterns per 1000 | 6.24 | 20.46 | 0.16 |
310
+ | Sentence length variance | 10.30 | 9.42 | -0.23 |
311
+ | Mean sentence length | 19.44 | 18.36 | -0.18 |
312
+
313
+ Lexical spread is the strongest correlate by some distance, and it holds up when length is
314
+ controlled, which matters because raw type-token ratio falls as text gets longer (r of -0.73 against
315
+ window length here). Measuring fixed 100-word chunks removes that.
316
+
317
+ Scored over whole documents with the shipped extraction, 15 human documents average 0.628 and none
318
+ exceeds 0.664, while 55 LLM documents average 0.685 and none falls below 0.658. The distributions
319
+ barely touch.
320
+
321
+ A second measure supports the reading. Taking the ten most frequent content words in a document, the
322
+ human corpus gives them 23.9% of all content tokens against 19.4%, 18.3% and 14.9% for the three LLM
323
+ corpora. Human technical writing concentrates on a small set of terms and repeats them. The Rust
324
+ ownership chapter leans on data, string, heap, memory and stack. That is terminology discipline, and
325
+ it is why the rule is worded as one name for one thing and never as a target number.
326
+
327
+ It ships as advisory. It describes a whole document and points at no line to fix, and padding with
328
+ repeated words would move it the right way while making the prose worse.
329
+
330
+ ## Re-running the study
331
+
332
+ The point of a measured style guide is that it can be re-measured. To recalibrate against a
333
+ different exemplar corpus, or to check whether a new candidate pattern earns its place:
334
+
335
+ 1. Collect exemplar prose in markdown, or adapt the stripping in `prose-check.mjs` for the format.
336
+ Prefer documents written before 2022, since anything newer may itself be LLM-drafted.
337
+ 2. Score both corpora with `--json`. Report the aggregate ratio by total words, and keep the
338
+ per-file rates for step 4.
339
+ 3. Keep a candidate only where the aggregate ratio clears 2×.
340
+ 4. Set `fail` to the lowest value that flags no human document, and `warn` to the human 90th
341
+ percentile. Report what fraction of LLM documents that catches. A threshold below the human
342
+ maximum is a bug, however good the aggregate ratio looks.
343
+ 5. Use at least three independent sources on each side. Two sources cannot tell a real pattern from
344
+ a house style.
345
+
346
+ A pattern that fires on prose you consider good is a bad pattern, however plausible the rule behind
347
+ it sounds. Every dropped candidate above sounded plausible, and two of them were already being
348
+ enforced as style rules.
349
+
350
+ ## On distilling from a rewriting tool
351
+
352
+ Feeding bad passages through a third-party rewriter and mining the diffs is a reasonable way to
353
+ generate candidate patterns. It is a poor way to decide which ones to keep, because the tool's own
354
+ preferences arrive along with the improvements, and there is no way to tell one from the other. The
355
+ measurement above is the filter. Generate candidates however you like, then keep the ones that
356
+ separate real human prose from your own output.