@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,293 @@
1
+ ---
2
+ name: isolated-testing-style
3
+ description: Write tests that start from given/when/then, use real collaborators through simulation rather than stubs and mocks, take their isolation from randomised data rather than shared setup and teardown, and assert behaviour rather than call counts. Use when writing or reviewing tests, when a test needs a collaborator faked, when reaching for a mock, spy, `toHaveBeenCalledWith`, `beforeEach`/`afterEach` fixtures or a hardcoded expected hash, when test setup has grown tangled, and when asked "how should I test this?".
4
+ license: Apache-2.0
5
+ metadata:
6
+ version: "1.13.1"
7
+ ---
8
+
9
+ # Isolated testing style
10
+
11
+ An opinionated way of writing tests. The examples are TypeScript and vitest, but the rules are about
12
+ test design and hold for any framework. Each rule exists because of a specific failure it would have
13
+ caught.
14
+
15
+ ## Start with Given, When, Then
16
+
17
+ Write the three comments before the test body, and before the code they will drive.
18
+
19
+ ```typescript
20
+ it("refuses an order once the offer has closed", async () => {
21
+ // Given an offer that closed while the customer was on the page.
22
+ // When the order is placed against it.
23
+ // Then it is refused rather than accepted late.
24
+ });
25
+ ```
26
+
27
+ Starting at this point, and not drifting to it, does three things.
28
+
29
+ **It lowers the cost of starting.** You only have to say what the situation is, what happens, and
30
+ what should result. That is a smaller question than "how do I test this?", and you can usually
31
+ answer it before you can answer the bigger one.
32
+
33
+ **It designs the interface.** Filling in `// When` forces you to name the single action under test,
34
+ in the caller's vocabulary. A step you cannot write as one `// When` usually means the interface is
35
+ wrong.
36
+
37
+ **It keeps the test readable as documentation.** Tests are read far more often than they are
38
+ written, and without the structure it is easy to produce a body where essential behaviour and
39
+ incidental setup look alike.
40
+
41
+ Then fill each comment in with the case it covers, and never with a restatement of the code:
42
+
43
+ ```typescript
44
+ it("refuses an order once the offer has closed", async () => {
45
+ // Given an offer that closed while the customer was on the page.
46
+ const offer = await offerFactory.make({ closesAt: aMinuteAgo });
47
+
48
+ // When the order is placed against it.
49
+ const placing = placeOrder(orderFactory.make({ offerId: offer.id }));
50
+
51
+ // Then it is refused rather than accepted late.
52
+ await expect(placing).rejects.toThrow(OfferClosedError);
53
+ });
54
+ ```
55
+
56
+ `// Given an offer` restates the code and adds no information.
57
+ `// Given an offer that closed while the customer was on the page` says which case this is and why
58
+ it matters. This holds whether or not the test is written first.
59
+
60
+ ## Prefer real collaborators through simulation
61
+
62
+ A stub asserts that your code called something. A simulator asserts that it called the service
63
+ correctly.
64
+
65
+ That difference is the whole argument. A stub answers whatever you told it to answer. It agrees with
66
+ your understanding of the API by construction. It cannot disagree with you, which means it cannot
67
+ find the case where your understanding is wrong. A simulator holds real state and applies the real
68
+ rules. A wrong call fails at the point the real service would have failed.
69
+
70
+ The evidence comes from a real project. Replacing AWS SDK stubs with a simulator immediately caught
71
+ two bugs that had already shipped.
72
+
73
+ - A Secrets Manager secret name ending in a hyphen and six characters. AWS appends exactly that
74
+ suffix to a secret ARN. The name was ambiguous with the ARN form, which AWS advises against. The
75
+ stub had no opinion, because a stub has no naming rules.
76
+ - A Cognito `SECRET_HASH` computed the wrong way. The stub accepted it, because the stub was never
77
+ going to check a signature.
78
+
79
+ So the order to reach for things:
80
+
81
+ 1. A simulator that holds real state and applies real rules.
82
+ 2. The real thing, when it runs in process and needs no external service.
83
+ 3. A stub, only for something with no rules worth modelling, such as a clock or a random source.
84
+
85
+ The first two options need the implementation to be swappable, so give each collaborator a single
86
+ point of entry, one place that wires the real service in production and a simulation in tests. The
87
+ driver pattern is one way to arrange that, but the name matters far less than the swap having one
88
+ home.
89
+
90
+ Simulating in process pays off beyond avoiding stubs. No deployment is needed before running the
91
+ tests, a debugger steps through the collaborator's state alongside your own, and no state is shared
92
+ between processes, so several layers can be exercised together and still run in parallel at the
93
+ speed of a unit test.
94
+
95
+ Be honest about the limit. An in-memory implementation only approximates the real service, and
96
+ cannot be relied on to behave identically. Keep a thin layer of tests against the real thing for the
97
+ flows where that matters, and treat any divergence you find as a bug in the simulation rather than a
98
+ quirk to work around.
99
+
100
+ ## Keep setup cheap and independent
101
+
102
+ Tangled shared fixtures come from economics. Discipline has little to do with it. Teams share setup
103
+ roughly in proportion to how expensive it is to build. When getting a test into the right state
104
+ means threading through a web of existing fixtures, reusing what is already there is the rational
105
+ move, and each reuse adds another edge to the graph. That is how a suite arrives at setup that no
106
+ one dares to touch.
107
+
108
+ The fix is to share differently. Shared factories for test entities are exactly what you want. A
109
+ factory that constructs a type is worth writing once and using everywhere. What has to be avoided is
110
+ those factories getting tangled up with each other. Each piece of setup should stand on its own.
111
+
112
+ Independence comes from taking dependencies explicitly rather than reaching for ambient state. A
113
+ factory that is handed what it needs stays pure, and the test decides what to hand it.
114
+ `@kensio/part-factory` builds this in. Factories take a `dependencies` object as a second argument
115
+ at call time. A factory that needs a simulated AWS is handed one, and never goes looking.
116
+
117
+ ```typescript
118
+ // Given an order that exists in this test's own simulated AWS.
119
+ const simAws = new SimAws();
120
+ const order = await orderFactory.make({ total: 5000 }, { simAws });
121
+ ```
122
+
123
+ A factory built that way can be shared as widely as you like and still stand on its own, because
124
+ everything it does is independent of what another factory did first. Prefer collaborators and
125
+ factories that need only instantiation, with no side effects, no cleanup and no coordination.
126
+
127
+ For a step specific to one test, ask whether the step belongs to the test or the test belongs to the
128
+ step. A helper confined to one file can be pulled back inline later if it stops earning its place,
129
+ whereas a fixture that a large part of the suite is built on cannot. Reversibility is the thing to
130
+ preserve, and locality on its own earns little.
131
+
132
+ ## Take isolation from randomised data
133
+
134
+ Randomised values make collisions impossible. No teardown is required, and there is no ordering to
135
+ depend on. Randomised is enough. Guaranteed uniqueness is unnecessary, and a UUID has no realistic
136
+ chance of colliding anyway.
137
+
138
+ Do not do this:
139
+
140
+ ```typescript
141
+ // Anti-pattern: shared name, mutable handle, teardown to undo it.
142
+ let bucketName: string;
143
+
144
+ beforeEach(async () => {
145
+ bucketName = "uploads-bucket";
146
+ await createBucket(bucketName);
147
+ });
148
+
149
+ afterEach(async () => {
150
+ await emptyBucket(bucketName);
151
+ await deleteBucket(bucketName);
152
+ });
153
+ ```
154
+
155
+ That test cannot run beside another test using the same name, the `let` is only mutable so that
156
+ `afterEach` can reach it, and a failure part way through leaves the next test to fail for a reason
157
+ unrelated to it.
158
+
159
+ Do this instead:
160
+
161
+ ```typescript
162
+ it("serves an uploaded object", async () => {
163
+ // Given a bucket no other test can be talking about.
164
+ const bucketName = `uploads-${faker.string.uuid()}`;
165
+ await createBucket(bucketName);
166
+
167
+ // ...
168
+ });
169
+ ```
170
+
171
+ The environment those tests run against can be shared, and should be built the way production is
172
+ built. A realistic environment that several tests read from is closer to production than a minimal
173
+ one rebuilt per test, and it is faster. The isolation comes from the names and identifiers, so
174
+ sharing the environment is free.
175
+
176
+ Faker is the source of these values. Prefer a generator that produces a realistic value of the right
177
+ kind (`faker.internet.email()`, `faker.string.uuid()`, `faker.company.name()`) over a counter or a
178
+ literal with a suffix. The test data also exercises the shapes production data has.
179
+
180
+ ## Assert observable behaviour
181
+
182
+ A call count asserts how the code is written today. A behaviour assertion holds however it is
183
+ written, which is what lets you refactor.
184
+
185
+ To prove a value is cached, do not count calls. Delete the underlying resource, then show the cached
186
+ value still comes back:
187
+
188
+ ```typescript
189
+ it("keeps serving the secret after it is deleted", async () => {
190
+ // Given a secret that has been read once, so it is cached.
191
+ const first = await config.databasePassword();
192
+
193
+ // When the underlying secret goes away.
194
+ await simAws.secretsManager().deleteSecret(
195
+ new DeleteSecretCommand({ SecretId: secretName }),
196
+ );
197
+
198
+ // Then the cached value is still served, without going back to the service.
199
+ expect(await config.databasePassword()).toEqual(first);
200
+ });
201
+ ```
202
+
203
+ To prove a retry, make the first call fail and the second succeed, then assert on the result:
204
+
205
+ ```typescript
206
+ // Given an endpoint that fails once and then works.
207
+ // When the client calls it.
208
+ // Then it gets the successful response.
209
+ ```
210
+
211
+ Both hold whether the cache is a `Map`, a memoised promise or a decorator, and whether the retry is
212
+ a loop, a middleware or a library.
213
+
214
+ ## Never pin a value computed by the code under test
215
+
216
+ Pinning a hash you generated by running the same function only proves the function is deterministic.
217
+ It will keep passing after the function becomes wrong, as long as it is wrong consistently.
218
+
219
+ ```typescript
220
+ // Anti-pattern: this string came from running computeSecretHash.
221
+ expect(computeSecretHash(username, clientId, clientSecret)).toBe(
222
+ "z0Xq9k1e4mVQ...",
223
+ );
224
+ ```
225
+
226
+ Two ways out, in order of preference:
227
+
228
+ 1. Let a real implementation validate it. A simulated Cognito checks a `SECRET_HASH` the way Cognito
229
+ checks it. A sign-in that succeeds against the simulation is evidence the hash is right.
230
+ 2. Pin against an independent authority, such as a value from the service's own documentation, a
231
+ published test vector, or a value produced by a different implementation.
232
+
233
+ The same rule covers snapshot tests of anything the code under test formats. A snapshot records what
234
+ the code does, and stays silent on what it should do.
235
+
236
+ ## Keep the top level to imports
237
+
238
+ In an ideal vitest or jest file, the only things outside the top-level `describe()` are the imports.
239
+ State, construction and helpers all live inside it. The file reads as a description of behaviour and
240
+ not as a program that happens to contain some tests.
241
+
242
+ ```typescript
243
+ import { describe, expect, it } from "vitest";
244
+ import { faker } from "@faker-js/faker";
245
+
246
+ import { placeOrder } from "./place-order";
247
+ import { offerFactory } from "./order.test-support";
248
+
249
+ describe("placing an order", () => {
250
+ // Everything else — state, helpers, tests — lives in here.
251
+ });
252
+ ```
253
+
254
+ This is mostly a consequence of the other rules and adds little on its own. What usually accumulates
255
+ at the top level of a test file is module-level state the tests share, a mutable handle that exists
256
+ so `afterEach` can reach it, and hoisted mock registrations. The rules above have already turned
257
+ down all three. So a top level that will not stay empty is a useful signal that something further up
258
+ has slipped. Imported factories are fine here. They arrive as imports precisely because they stand
259
+ on their own.
260
+
261
+ ## Put test support beside the code
262
+
263
+ A test file should hold tests. Where support lives depends on what it is for.
264
+
265
+ A factory for a **type** belongs beside the type it constructs, exported. That no consumer ever
266
+ hand-rolls the literal:
267
+
268
+ ```
269
+ src/orders/
270
+ ├── order.ts
271
+ ├── order.test.ts
272
+ └── order.test-support.ts # factories and helpers for order.ts
273
+ ```
274
+
275
+ A library that defines a shape other code has to construct should export the factory for it.
276
+
277
+ A step written for **one test** stays in that test's file, inside the `describe`. Promoting it later
278
+ when another test wants it is fine. Make it independent first, so what spreads is a self-contained
279
+ factory (not a dependency on how some other test left things).
280
+
281
+ If a test file is mostly setup, that is a signal. The fix is usually cheaper construction. A shared
282
+ fixture treats the symptom. Scroll the file and see how much of it is `it(...)` bodies making
283
+ assertions.
284
+
285
+ ## Tools that help
286
+
287
+ These serve the style. The style holds without them.
288
+
289
+ - [Faker](https://fakerjs.dev/) for randomised, realistic values.
290
+ - [`@kensio/part-factory`](https://partfactory.dev/) for typed factories that need only
291
+ instantiation. See the `part-factory-test-data` skill.
292
+ - [`@kensio/yulin`](https://yulinsim.dev/) simulates AWS in process, when AWS is the collaborator.
293
+ See the `yulin-aws-simulation` skill.
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: pangram-check
3
+ description: Send the prose of a finished document to Pangram, a commercial AI-text detector, and report which passages read as machine-drafted, with the source line each one starts on. Use when the user asks to "check this with Pangram", "run the AI detector over this", "see if this still reads as AI", or wants a final sweep after rewriting a draft. Every run is paid and sends the writing to a third party, so this skill is invoked by name.
4
+ license: Apache-2.0
5
+ compatibility: Needs a Pangram API key and network access. Every run is billed.
6
+ metadata:
7
+ version: "1.13.1"
8
+ disable-model-invocation: true
9
+ ---
10
+
11
+ # Pangram check
12
+
13
+ A final sweep over a finished document. Pangram scores the text in windows of a few hundred words
14
+ and reports which parts read as machine-drafted, so a long document gets a passage-by-passage
15
+ answer.
16
+
17
+ ```bash
18
+ node scripts/pangram-check.mjs path/to/file.md
19
+ ```
20
+
21
+ Full flag list and config file in [references/configuration.md](references/configuration.md). Field
22
+ meanings and how to read a verdict in
23
+ [references/reading-results.md](references/reading-results.md).
24
+
25
+ ## Confirm before spending a call
26
+
27
+ Every run costs the user money and sends their writing to a third party. Before the first call of a
28
+ session:
29
+
30
+ 1. Name the file that is about to go out.
31
+ 2. Give the word count and the billable estimate. `--dry-run` prints both and sends nothing.
32
+ 3. Wait for a yes.
33
+
34
+ This skill sets `disable-model-invocation: true`. It runs when a user asks for it by name, and never
35
+ as a step some other task decided to take. A skill that drafts or rewrites text may mention it and
36
+ stop there.
37
+
38
+ ## Check the key first
39
+
40
+ On a machine that has not run this before:
41
+
42
+ ```bash
43
+ node scripts/pangram-check.mjs --check-key
44
+ ```
45
+
46
+ That checks the key against `GET /models` and spends no detection call. With no key found, the
47
+ script prints where it looked and how to store one. Pass that message on as it stands. Never print,
48
+ echo or paste the key itself, and never put it in a shell command the user will see.
49
+
50
+ The key is read from `$PANGRAM_API_KEY`, then `$PANGRAM_ENV_FILE`, then `~/.config/pangram/.env`,
51
+ then `./.env`. Recommend the third one, which every repository on the machine can reach.
52
+
53
+ ## Only the prose is sent
54
+
55
+ The document is reduced to its prose before the call, and that reduced text is what Pangram
56
+ receives. Frontmatter, code blocks, HTML comments, images, shortcodes, raw HTML, tables, headings
57
+ and URLs are all removed. Link text and inline code keep their words.
58
+
59
+ Check what would go out with `--print-prose`. The word count the script reports is that same string,
60
+ so a document reporting `512 words of prose` sent exactly those 512 words whatever the file length.
61
+
62
+ This matters for reading the verdict. A page that is mostly code would otherwise have its bash and
63
+ JSON scored alongside the writing, which pulls the fractions towards human for reasons unrelated to
64
+ how the prose reads.
65
+
66
+ Wrap quoted material in `<!-- pangram-check:off -->` and `<!-- pangram-check:on -->` to keep someone
67
+ else's writing out of the score.
68
+
69
+ ## The guards
70
+
71
+ Two of them refuse to send anything.
72
+
73
+ - **Under 50 words of prose:** Pangram's own documented floor, below which it declines to predict.
74
+ - **Under `--min-words`** (300 by default). Pangram predicts from 50 words up with less confidence
75
+ the shorter the text. Lower the floor for a short document, and treat the verdict as weaker.
76
+
77
+ A repository can add its own with `rejectPatterns` in `.pangram-check.json`, or `--reject <regex>`
78
+ for one run. `--reject-todo` is the preset for the common case, where a `TODO` marker means a
79
+ paragraph is still to be written and scoring it would spend a paid call on a draft.
80
+
81
+ `--max-units <n>` refuses a document over a cost ceiling. One billable unit is each started block of
82
+ 1000 words.
83
+
84
+ ## Repeat runs are free
85
+
86
+ Results are cached by a hash of the extracted prose, so running the same unchanged document again
87
+ reads the cache and calls nothing. Edit the prose and the next run is a fresh call. `--refresh`
88
+ forces one, `--no-cache` skips the cache in both directions.
89
+
90
+ There is deliberately no score history and no before-and-after delta. Both would invite treating the
91
+ number as a target.
92
+
93
+ ## Output
94
+
95
+ `--format text` (the default) colour-codes scores on a terminal and draws the windows in reading
96
+ order. `--format markdown` gives a table to paste into a report. `--format json` carries every field
97
+ plus the source line for each window.
98
+
99
+ ## Reporting back
100
+
101
+ Give the user the headline, the three fractions, and the worst two or three windows with their
102
+ scores and line references. Point at the passages by line so they can open them.
103
+
104
+ Leave the judgement about rewriting to the user. A high score marks a passage worth rereading, and
105
+ it is not a number to drive down. Editing to move a detector score is a different activity from
106
+ writing in your own voice, and the two come apart quickly.
107
+
108
+ ## Limits
109
+
110
+ Pangram reports whether text reads as machine-generated. It has no opinion on whether the writing is
111
+ any good, and a document can come back fully human while being badly organised.
112
+
113
+ A `is_humanized` flag on a window is a stronger signal than a high score by itself, because it means
114
+ the passage looks like text that has been worked over to read as human.
115
+
116
+ Detectors carry false positives, which fall hardest on writers working in a second language. Treat
117
+ one verdict as one input to a rereading, never as proof of authorship, and say so if a user starts
118
+ treating it as proof.
119
+
120
+ ## Related skills
121
+
122
+ `technical-prose-style` covers a different question. It measures the constructions that make prose
123
+ tiring to read, and its own measurements show that text following every one of its rules is still
124
+ identified as machine-written. Style and provenance are separate signals. Anyone after concealment
125
+ is holding the wrong tool.
@@ -0,0 +1,120 @@
1
+ # Configuration
2
+
3
+ Every setting has a flag. The ones a repository wants every time belong in a config file.
4
+
5
+ ## Flags
6
+
7
+ | Flag | Effect |
8
+ | ------------------ | ---------------------------------------------------------------- |
9
+ | `--dry-run` | Runs the guards, prints the cost estimate, sends nothing |
10
+ | `--print-prose` | Prints the extracted prose and exits |
11
+ | `--format <f>` | `text` (default), `markdown` or `json` |
12
+ | `--windows <n>` | How many windows to detail, or `all` (default 5) |
13
+ | `--min-words <n>` | Refuses below this many words of prose (default 300) |
14
+ | `--max-units <n>` | Refuses over this many billable units |
15
+ | `--reject-todo` | Refuses while the file holds `TODO` markers |
16
+ | `--reject <regex>` | Refuses when the pattern matches the file. Repeatable |
17
+ | `--skip-quotes` | Leaves blockquoted material out of the prose |
18
+ | `--plain` | Treats the file as plain text, skipping the markdown rules |
19
+ | `--model <name>` | Pangram model selector, passed through as `model` |
20
+ | `--list-models` | Lists the selectors the key allows, then exits |
21
+ | `--check-key` | Checks the key is accepted, then exits. Spends no detection call |
22
+ | `--refresh` | Ignores any cached result for this text |
23
+ | `--no-cache` | Neither reads nor writes the cache |
24
+ | `--config <path>` | Uses this config file |
25
+ | `--no-config` | Ignores any `.pangram-check.json` |
26
+ | `--no-color` | Plain output on a terminal |
27
+
28
+ Exit codes: `0` ran, `1` a guard refused or Pangram failed, `2` a usage, config or key problem.
29
+
30
+ ## The config file
31
+
32
+ `.pangram-check.json`, found by walking up from the file being checked. The first one found wins,
33
+ and flags override it. An unknown key prints a warning and is ignored.
34
+
35
+ ```json
36
+ {
37
+ "minWords": 300,
38
+ "maxUnits": 2,
39
+ "rejectTodo": true,
40
+ "rejectPatterns": ["<!--\\s*FIXME", "^\\s*XXX\\b"],
41
+ "skipQuotes": false,
42
+ "format": "markdown",
43
+ "windows": 3,
44
+ "model": "default",
45
+ "cache": true
46
+ }
47
+ ```
48
+
49
+ Patterns are JavaScript regular expressions, matched against the raw file with the `g` and `m`
50
+ flags, so `^` and `$` anchor to a line. Backslashes need escaping for JSON. Every match is reported
51
+ with its line number.
52
+
53
+ `rejectTodo` is the preset behind `--reject-todo`, and matches a `TODO` opening an HTML comment or a
54
+ line.
55
+
56
+ ## The API key
57
+
58
+ Read from the first of these that carries `PANGRAM_API_KEY`, and never printed:
59
+
60
+ 1. `$PANGRAM_API_KEY` in the environment
61
+ 2. `$PANGRAM_ENV_FILE`, a path to a dotenv file
62
+ 3. `~/.config/pangram/.env` (or `$XDG_CONFIG_HOME/pangram/.env`)
63
+ 4. `./.env` in the working directory
64
+
65
+ The third is the one to recommend. It sits outside any repository and every project on the machine
66
+ can reach it.
67
+
68
+ ```bash
69
+ mkdir -p ~/.config/pangram
70
+ printf 'PANGRAM_API_KEY=%s\n' 'the-key' > ~/.config/pangram/.env
71
+ chmod 600 ~/.config/pangram/.env
72
+ ```
73
+
74
+ A placeholder value (anything starting `replace`, `your` or `<`) counts as absent. The key is
75
+ redacted from any error text the script prints.
76
+
77
+ `$PANGRAM_API_BASE` overrides the API host. The test stub in this repository uses it.
78
+
79
+ ## What counts as prose
80
+
81
+ These are removed entirely:
82
+
83
+ - YAML and TOML frontmatter
84
+ - Fenced and indented code blocks
85
+ - HTML comments
86
+ - Images, tables, headings and horizontal rules
87
+ - Link reference definitions
88
+ - Hugo shortcodes and Liquid tags
89
+ - HTML and JSX tags
90
+ - Bare URLs
91
+
92
+ These keep their words and lose their markup:
93
+
94
+ - Link text
95
+ - Inline code
96
+ - Bold and emphasis
97
+ - Footnote text
98
+ - List item text
99
+ - Blockquotes (unless `--skip-quotes`)
100
+
101
+ Each paragraph and each list item becomes one block. Blocks are joined with a space, and a block
102
+ with no terminal punctuation gains a full stop. Without it the detector reads a run of list items as
103
+ one sentence.
104
+
105
+ Regions between `<!-- pangram-check:off -->` and `<!-- pangram-check:on -->` are dropped. Use them
106
+ for quoted material.
107
+
108
+ `.md`, `.markdown`, `.mdx` and `.mdoc` get the markdown rules. Everything else is treated as plain
109
+ text, and `--plain` forces that.
110
+
111
+ Every block records the source line it started on. That is where the `file:line` reference on each
112
+ reported window comes from.
113
+
114
+ ## The cache
115
+
116
+ `$XDG_CACHE_HOME/pangram-check/` or `~/.cache/pangram-check/`, one JSON file per result, keyed by a
117
+ SHA-256 of the model selector and the extracted prose. The file holds the full API response,
118
+ including the submitted text.
119
+
120
+ Delete the directory to clear it. `--no-cache` writes nothing there.
@@ -0,0 +1,69 @@
1
+ # Reading a result
2
+
3
+ Pangram scores the whole submission and then scores it again in windows, so the useful part of a
4
+ result is the window list.
5
+
6
+ ## The document level
7
+
8
+ | Field | What it carries |
9
+ | -------------------------- | --------------------------------------------------------- |
10
+ | `headline` | A short verdict, such as `AI Detected` or `Human Written` |
11
+ | `prediction` | A sentence of detail behind the headline |
12
+ | `prediction_short` | The same in a word or two |
13
+ | `fraction_ai` | Share of the text scored as machine-written, 0 to 1 |
14
+ | `fraction_ai_assisted` | Share scored as written with machine help |
15
+ | `fraction_human` | Share scored as human-written |
16
+ | `num_ai_segments` | Count of segments in each class, alongside the two below |
17
+ | `num_ai_assisted_segments` | |
18
+ | `num_human_segments` | |
19
+ | `version` | The detector version behind the verdict |
20
+
21
+ The three fractions and the three segment counts answer different questions. A document can be 20%
22
+ AI by volume while only one segment out of nine is the source of it, which points at a passage. The
23
+ same 20% spread evenly across every segment points at the whole document.
24
+
25
+ ## The window level
26
+
27
+ | Field | What it carries |
28
+ | ---------------------------- | ------------------------------------------------- |
29
+ | `ai_assistance_score` | 0 for human, 1 for machine. The number to sort by |
30
+ | `label` | `Human`, `AI` or `AI Assisted` |
31
+ | `confidence` | How sure the detector is of that label |
32
+ | `text` | The passage that was scored |
33
+ | `start_index`, `end_index` | Character offsets into the submitted prose |
34
+ | `word_count`, `token_length` | The size of the window |
35
+ | `is_humanized` | The passage looks worked over to read as human |
36
+ | `humanizer_score` | How strongly, 0 to 1 |
37
+
38
+ The script turns `start_index` into a `file:line` reference by mapping it back through the
39
+ extraction, so a flagged window points at the paragraph in the source file. Line references are
40
+ accurate to the block, and the column is not tracked.
41
+
42
+ ## Which signals to act on
43
+
44
+ **Sort by `ai_assistance_score` and read the top two or three passages.** Everything else in the
45
+ result is context for those.
46
+
47
+ **`is_humanized` outranks a high score.** A high score says a passage reads as machine-written. The
48
+ humanized flag says it reads as machine-written text that has since been edited to look human, which
49
+ is a claim about the editing as well as the drafting.
50
+
51
+ **`confidence` qualifies the label, and it qualifies a low score too.** A `Human` label at low
52
+ confidence is a weaker result than a `Human` label at high confidence.
53
+
54
+ **Short windows are noisier.** A window of 60 words carries less signal than one of 300. Check
55
+ `word_count` before acting on an outlier.
56
+
57
+ ## The limits of a verdict
58
+
59
+ Pangram reports whether text reads as machine-generated. Whether the writing is any good is a
60
+ separate question, and a document can come back fully human while being badly organised, wrong, or
61
+ dull.
62
+
63
+ Detectors carry false positives. Published audits have found high false-positive rates on writing by
64
+ non-native English speakers, which is the failure mode to hold in mind before treating a score as
65
+ evidence about a person. One verdict is one input to a rereading.
66
+
67
+ Scores also move for reasons that have nothing to do with quality. Quoted material, standard
68
+ technical phrasing and heavily edited passages all shift the number. Read the passage the score
69
+ points at, and judge the passage.