speccle 0.11.0 → 0.12.0

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,163 @@
1
+ ---
2
+ name: spec-feature
3
+ description: Draft or amend a feature's markdown contract — SPEC.md, CONTEXT.md, AGENTS.md, decisions/ — from a plan or any raw input, lint it clean with the oracle (product-voiced When/Then statements included), and hand back the criteria, without writing any tests or code. Use when the user wants a spec drafted or amended but not yet implemented, wants acceptance criteria written for a behaviour, or says "spec this", "draft the criteria", "add a criterion to this slice".
4
+ allowed-tools: Read(/${CLAUDE_PLUGIN_ROOT}/skills/*/references/**)
5
+ ---
6
+
7
+ # spec-feature
8
+
9
+ Produce the markdown contract — `SPEC.md`, `CONTEXT.md`, `AGENTS.md`, `decisions/` —
10
+ for one feature, and lint it clean. Stage 2 of the `feature` pipeline, and complete
11
+ on its own when the user wants a spec without an implementation. This skill writes
12
+ markdown only — tests and code are `implement-feature`'s job.
13
+
14
+ In the pipeline this skill runs in its own subagent session: the hand-off in the
15
+ prompt and the feature folder on disk are everything — there is no planning
16
+ conversation to consult, and nobody to ask mid-run. The final message is the
17
+ hand-back.
18
+
19
+ The shape of the contract is fixed by the convention, bundled beside this skill. Read
20
+ `${CLAUDE_SKILL_DIR}/references/convention.md` before drafting — it is the written
21
+ contract, and this skill only restates the parts drafts get wrong.
22
+
23
+ Speccle's words are fixed and mandatory: "criterion id", not "tag"; "statement", not
24
+ "title"; "body", not "notes"; "lint violation", not "error" or "warning"; "amend",
25
+ not "edit" or "update".
26
+
27
+ ## 1. Start from a plan
28
+
29
+ In the pipeline, the hand-off carries the route, the feature folder, the key, the
30
+ scope, and every key decision the plan settled — each marked agreed or defaulted, the
31
+ cross-criterion ones already captured as ADRs in the slice's `decisions/`, the
32
+ per-behaviour ones noted for a criterion body. Land those notes where they belong as
33
+ you draft; flag anything marked defaulted again in your hand-back. A decision the
34
+ hand-off genuinely leaves open is taken on its recommendation and flagged defaulted —
35
+ never guessed silently, and never a reason to stall.
36
+
37
+ Standalone, settle those the way `plan-feature` does — route on where the behaviour
38
+ lives, read every other `SPEC.md`'s frontmatter before picking a key, and put key
39
+ decisions the input leaves open to the user before drafting. Record each by the
40
+ routing rule below: spanning criteria → an ADR in `decisions/`, about one behaviour →
41
+ that criterion's body. No key decision lives only in the conversation.
42
+
43
+ If the input is already a conventioned `SPEC.md`, adopt it as-is. Do not "improve"
44
+ the criteria; the human already owns them.
45
+
46
+ ## 2. Draft (new) or amend the contract
47
+
48
+ **New route** — scaffold the convention's shape: `SPEC.md`, `CONTEXT.md`, and
49
+ `AGENTS.md` at the feature root, `decisions/` when the first cross-criterion choice
50
+ lands. The parts worth restating because they are where drafts go wrong:
51
+
52
+ - **Statements speak product, When/Then by default.** "When X, Y" — the trigger,
53
+ then the outcome, readable by someone who has never seen the code. A simple
54
+ invariant may stay plain declarative ("An empty basket totals zero"). Code-level
55
+ precision that is genuinely contractual — exact messages, ordering, rounding,
56
+ identifiers — goes in the body, never the statement; the `code-voice` lint rule
57
+ enforces the boundary.
58
+ - **A statement is one testable clause.** If you cannot picture the single assertion
59
+ that fails when it breaks, it is not one criterion. "Tax rounds half-up and the
60
+ basket rejects over 100 items" is two criteria wearing one heading.
61
+ - **Ids are names, not order.** A new criterion takes the next never-used number under
62
+ its key. Never renumber, never reuse — deleting `[CHECKOUT-2]` retires that number.
63
+ - **The body is free and never linted.** Rationale, edge cases, worked examples.
64
+ Prefer a worked number over an adjective — and make it one the reader cannot
65
+ misread: "three line items of £1.99 at 20% → £1.20 tax; taxing the £5.97 basket
66
+ total would give £1.19" beats "3 × £1.99 → £1.20", which silently depends on
67
+ whether that is one line item or three.
68
+ - **The routing rule.** About a word → `CONTEXT.md`, a glossary only — every term
69
+ gets an _Avoid_ line naming the synonyms the feature will not use.
70
+ About one behaviour → that criterion's body. A choice spanning criteria → an ADR in
71
+ `decisions/`.
72
+ - **`SPEC.md`'s intro prose carries the product intent** — what the feature is for
73
+ and its scope, a paragraph, not a line. There is no separate product document; this
74
+ is where a future reader learns why the slice exists.
75
+ - **`AGENTS.md` states how to work the slice**, not what it does: how to run its
76
+ tests, and where the contract lives. Behaviour stays in `SPEC.md` — duplicating it
77
+ here is drift waiting to happen.
78
+
79
+ `SPEC.md`, `CONTEXT.md`, and `AGENTS.md` are the floor, even for a tiny feature;
80
+ `decisions/` appears with the first decision.
81
+
82
+ **Amend route** — the contract already exists; change it in place, never re-scaffold:
83
+
84
+ - **New criteria take the next never-used numbers** under the slice's existing key.
85
+ Read the whole spec first — including the numbers no longer present; a deleted id
86
+ stays retired.
87
+ - **A reworded statement keeps its id only if it promises the same behaviour**,
88
+ sharpened. A statement that now promises something different is a retire-plus-draft:
89
+ the old id goes, a new id arrives, and the old id's tests go with it (flag them for
90
+ `implement-feature`).
91
+ - **Adopt the slice's language.** Read its `CONTEXT.md` and write new criteria in
92
+ those terms; add a glossary entry only for a genuinely new term, with its _Avoid_
93
+ line. Introducing a synonym for an existing term is the drift the glossary exists
94
+ to prevent.
95
+ - **A change that spans criteria gets an ADR** in the slice's `decisions/`, same as
96
+ on the new route.
97
+ - **Leave unrelated criteria untouched.** The diff of an amendment reads as the
98
+ request: statements the request did not mention do not change.
99
+
100
+ ## 3. Lint until clean
101
+
102
+ Resolve the oracle once, in this order, and reuse what works:
103
+
104
+ 1. `<repo-root>/node_modules/.bin/speccle` — the repo's own pinned copy, put
105
+ there by `strength init`. A devDependency is never on `PATH` in this shell, so test
106
+ for the file; in a monorepo check the package you are working in as well as the
107
+ root. It wins over a global install: the pin is a committed choice, and lint rules
108
+ change between versions.
109
+ 2. `speccle` on `PATH` — a global install.
110
+ 3. Otherwise, from a clone of the speccle repo, run it from source — Node ≥ 24 executes
111
+ TypeScript directly, so no build is needed:
112
+ `node <speccle-repo>/packages/oracle/src/cli.ts`.
113
+
114
+ If none resolves, point the user at the install steps in Speccle's README and stop.
115
+ Do not hand-check the convention in the oracle's place: a spec that has not been
116
+ linted has not been linted, and claiming otherwise is the one thing this workflow
117
+ cannot survive.
118
+
119
+ ```sh
120
+ <oracle> lint <feature-folder>
121
+ ```
122
+
123
+ Fix and re-run until it reports clean. Exit `0` clean, `1` violations, `2` usage
124
+ error. `--json` gives `{ root, files, violations, clean }` with each violation
125
+ carrying `rule`, `file`, `line`, `message` — parse that rather than scraping the
126
+ human output.
127
+
128
+ Quality violations (`weasel-wording`, `compound-criterion`, `unmeasurable`,
129
+ `code-voice`) judge the heading statement only. When one fires, rewrite the
130
+ statement — do not move the offending words down into the body to dodge the rule.
131
+ Two of them deserve their own note:
132
+
133
+ - A `compound-criterion` usually means you owe the spec a second criterion, not a
134
+ shorter sentence.
135
+ - A `code-voice` violation means the statement names the mechanism. Push the code
136
+ fragment into the body — where that precision belongs — and say what the user
137
+ observes instead: "When a basket exceeds 100 line items, checkout rejects it", not
138
+ "`addItem` throws `BasketLimitError`".
139
+
140
+ `unmeasurable` does not police vocabulary — any domain verb passes ("a refund
141
+ **credits** the customer…"). It fires only on a statement that asserts nothing: a
142
+ vacuous predicate ("refunds **are handled**") or a bare property ("the dashboard **is
143
+ beautiful**"). Say what is observably true instead; the rule is telling you the
144
+ criterion has no outcome to test, not that it dislikes your wording.
145
+
146
+ The rules are fixed and unconfigurable, and there is one severity: a spec lints clean
147
+ or it does not.
148
+
149
+ ## 4. Hand back the criteria
150
+
151
+ In the pipeline, the hand-back is your final message: every criterion drafted,
152
+ amended, or retired — ids and statements — every decision recorded and where, and
153
+ each one flagged that was defaulted rather than agreed. The orchestrator shows it to
154
+ the human; you do not wait, and you do not ask.
155
+
156
+ Standalone, the same content is the **spec summary**, announced in chat: the human
157
+ owns the criteria, and that ownership is exercised by interrupting or amending — not
158
+ at a blocking pre-approval. Treat "looks good, and also…" as a change request: amend
159
+ the spec, re-lint, announce again. Say what the contract does not yet have: tests
160
+ and code are `implement-feature`'s job, and nothing is claimed until it runs.
161
+
162
+ When the input was already a conventioned `SPEC.md` adopted unchanged, there is
163
+ nothing new to announce; say you adopted it and move on.
@@ -0,0 +1,239 @@
1
+ <!-- Generated from docs/convention.md by scripts/sync-plugin-references.mjs — do not edit.
2
+ Edit the source and run `pnpm sync:plugin-refs`. -->
3
+
4
+ # The Speccle convention
5
+
6
+ The written contract every skill and every Speccle tool implements. Terminology is
7
+ defined in `CONTEXT.md`; decisions behind this shape are in
8
+ `docs/adr`.
9
+
10
+ ## The feature folder
11
+
12
+ A feature is a directory **named for the feature**, owning one vertical slice. The name
13
+ announces the slice — `scanner/`, never an unnamed catch-all like `src/` or `lib/` —
14
+ and holds even when a project has only one feature: the slice is self-announcing, and a
15
+ second feature has an obvious home beside it.
16
+
17
+ Every feature folder has the same shape:
18
+
19
+ ```
20
+ scanner/
21
+ ├── SPEC.md ← the acceptance criteria
22
+ ├── CONTEXT.md ← the feature's language (a glossary, nothing else)
23
+ ├── AGENTS.md ← how an agent works the slice
24
+ ├── decisions/ ← the feature's ADRs, one file per decision
25
+ │ └── 0001-<slug>.md
26
+ └── src/
27
+ ├── scanner.ts
28
+ └── scanner.test.ts
29
+ ```
30
+
31
+ The root holds the markdown contract and nothing else. All code and tests sit in
32
+ `src/`, tests beside the code they defend, and the code subfolder is always named
33
+ `src` — every feature folder in every project opens the same way. `decisions/` appears
34
+ with the feature's first decision; the other four entries are the floor, even for a
35
+ tiny feature.
36
+
37
+ Inside `src/`, subfolders are allowed but not the default
38
+ (ADR-0037). Keep
39
+ `src/` flat while it holds **ten files or fewer** directly — count the code and test
40
+ files sitting in it, subfolders excluded. The file that would make it eleven triggers
41
+ grouping: gather the code into shallow (prefer one level), purpose-named subfolders,
42
+ and the same ten-file limit applies within each subfolder. A `src/` that has grown into
43
+ a pile is often a signal to split the slice into siblings instead; nest when it is
44
+ genuinely one cohesive feature that simply carries many files. Tests stay beside the
45
+ code they defend at whatever depth it sits.
46
+
47
+ Everything about the feature lives inside the subtree. An agent landing in the folder
48
+ needs nothing else to understand it — and `AGENTS.md` is where it starts reading.
49
+
50
+ ## SPEC.md
51
+
52
+ ```markdown
53
+ ---
54
+ key: CHECKOUT
55
+ ---
56
+
57
+ # Checkout
58
+
59
+ Optional intro prose about the feature.
60
+
61
+ ## [CHECKOUT-1] When a line item is taxed, tax rounds half-up
62
+
63
+ Tax is computed per line item and rounded half-up to 2dp before summing.
64
+
65
+ Edge cases:
66
+
67
+ - three line items of £1.99 at 20% → £1.20 tax; taxing the £5.97 basket total would give
68
+ £1.19
69
+
70
+ ## [CHECKOUT-2] An empty basket totals zero
71
+ ```
72
+
73
+ Rules:
74
+
75
+ 1. **Frontmatter declares the feature key**: `key: <KEY>` where `<KEY>` matches
76
+ `[A-Z][A-Z0-9]{1,9}`. Keys are unique across the repo.
77
+ 2. **Each criterion is an H2**: `## [KEY-n] <statement>`. The statement is one testable
78
+ clause — single behaviour, no weasel words, measurable.
79
+ 3. **Statements speak product, defaulting to a When/Then shape**
80
+ (ADR-0032):
81
+ "When X, Y" — the trigger, then the outcome, readable without seeing code. A simple
82
+ invariant may stay plain declarative (`An empty basket totals zero`). Code-level
83
+ precision that is contractual — exact messages, ordering, regexes — lives in the
84
+ body, never the statement.
85
+ 4. **The body is free.** Rationale, edge cases, examples — anything about that one
86
+ behaviour. A body may be empty.
87
+ 5. **Ids are names, not order.** Document position carries order. A new criterion takes
88
+ the next never-used number under its key. An id is never renumbered or reused —
89
+ deleting `[CHECKOUT-2]` retires the number forever.
90
+ 6. **H2s in a spec are criteria.** Non-criterion structure belongs in intro prose,
91
+ criterion bodies, or `CONTEXT.md`.
92
+
93
+ ## CONTEXT.md (per feature)
94
+
95
+ The feature's glossary, in the style of this repo's own root `CONTEXT.md`: the domain
96
+ language — each term defined once, with an _Avoid_ line naming the synonyms not to use.
97
+
98
+ A glossary only. Decisions never live here — they are ADRs in `decisions/`
99
+ (ADR-0021). The
100
+ boundary: about a word → `CONTEXT.md`; about one behaviour → that criterion's body in
101
+ `SPEC.md`; a choice spanning criteria → `decisions/`.
102
+
103
+ ## AGENTS.md (per feature)
104
+
105
+ The slice's agent-facing entry point, following the cross-tool convention of the same
106
+ name. It orients in one screenful, stating only what the folder cannot show:
107
+
108
+ - what the slice does — a sentence or two
109
+ - how to run its tests (and any other commands the slice needs)
110
+ - where the contract lives: `SPEC.md` for the criteria, `CONTEXT.md` for the language,
111
+ `decisions/` for the choices — and that tests claim criteria by `[KEY-n]` token
112
+
113
+ Facts about _working_ the slice belong here; facts about the feature's _behaviour_
114
+ belong in `SPEC.md`. Duplicating either in the other is drift waiting to happen.
115
+
116
+ ## decisions/ (per feature)
117
+
118
+ The feature's architecture decision records: choices that span criteria, one numbered
119
+ file per decision — `decisions/0001-<slug>.md`, in the same form as any ADR (status,
120
+ context, decision, consequences). The folder appears with the first decision. A choice
121
+ about one behaviour is not a decision record — it is that criterion's body.
122
+
123
+ ## Test linking
124
+
125
+ A test defends a criterion when the criterion id appears anywhere in its **full
126
+ concatenated name** — enclosing group titles included. One
127
+ `describe('[CHECKOUT-1] tax rounding', …)` claims every test inside it. Mutation and
128
+ coverage reports already carry full names, so the join is mechanical.
129
+
130
+ What counts as a test file, and what counts as its name, is the **test dialect**'s
131
+ business (ADR-0038).
132
+ Where a framework gives a test a string name, the id is claimed bracketed —
133
+ `[CHECKOUT-1]`. Where the name _is_ an identifier, the id takes its identifier-safe
134
+ spelling: `func test_CHECKOUT_1_taxRounds()` claims `CHECKOUT-1`. The two spellings are
135
+ one id, not two — `SPEC.md` headings and every report use the bracketed form
136
+ (ADR-0039).
137
+
138
+ ## Spec discovery
139
+
140
+ Tools find every `SPEC.md` under the target root. Directories named `node_modules`,
141
+ `dist`, `fixtures`, or `__fixtures__` — and dot-directories — are never entered. The
142
+ skip list is fixed, not configuration
143
+ (ADR-0016): a project that
144
+ fixtures deliberately dirty specs for its own tests still lints clean at its root,
145
+ and a feature directory may not take one of the skipped names.
146
+
147
+ ## Lint
148
+
149
+ `oracle lint` enforces this contract deterministically. Fixed rules, one severity, no
150
+ configuration. The rule set:
151
+
152
+ | Rule | Judges |
153
+ | -------------------- | -------------------------------------------------------------- |
154
+ | `missing-key` | Frontmatter `key` absent or malformed |
155
+ | `key-collision` | Two specs declare the same key |
156
+ | `key-mismatch` | A criterion id's key differs from the spec's declared key |
157
+ | `malformed-id` | H2 without a well-formed `[KEY-n]` token |
158
+ | `duplicate-id` | The same id appears twice |
159
+ | `empty-statement` | Criterion heading has a token but no statement |
160
+ | `weasel-wording` | Statement hedges (`should`, `appropriately`, `as expected`, …) |
161
+ | `compound-criterion` | Statement contains more than one testable clause |
162
+ | `unmeasurable` | Statement asserts nothing observable |
163
+ | `code-voice` | Statement reads as code rather than product language |
164
+
165
+ Quality rules (`weasel-wording`, `compound-criterion`, `unmeasurable`, `code-voice`)
166
+ judge the heading statement only — the body is never linted.
167
+
168
+ `compound-criterion` exempts one bare `and`/`or` — a compound noun phrase names one
169
+ thing (`restores stock and credit`). A second bare conjunction in the main clause flags:
170
+ it reads as a list of behaviours (`restores stock and credits the customer and emails
171
+ them`). Conjunctions inside a condition (`when the card is expired and the retry limit
172
+ is reached`) qualify one outcome and are not counted.
173
+
174
+ `unmeasurable` never allow-lists the verbs a statement may use. It flags a closed list of
175
+ predicates that name activity without an outcome (`is handled`, `works`, `supports`, …)
176
+ and main clauses that name a property (`The dashboard is beautiful`). Any other verb
177
+ passes, including a domain verb the rule has never seen (`a refund credits the
178
+ customer`). It under-flags by design
179
+ (ADR-0010).
180
+
181
+ `code-voice` judges the raw statement — here a code span is the strongest signal, not a
182
+ stripped literal. Signals, first match wins: a code span; a file path, recognised by a
183
+ closed extension list (`cjs`, `css`, `html`, `js`, `json`, `jsx`, `md`, `mjs`, `sh`,
184
+ `toml`, `ts`, `tsx`, `txt`, `yaml`, `yml`); an identifier — camelCase with at least two
185
+ leading lowercase letters (brand names like iPhone pass), snake_case, or call
186
+ parentheses. Plain acronyms (JSON, HTTP) and unlisted extensions pass — the rule
187
+ under-flags by design
188
+ (ADR-0032).
189
+
190
+ ## Supported stacks
191
+
192
+ Speccle is **multi-language across a supported set, not language-agnostic**. A repo
193
+ declares which test dialect it is on; it never declares how that dialect works, so a
194
+ clean `claims` run means the same thing in every repo
195
+ (ADR-0038). An
196
+ unsupported stack is unsupported, and says so.
197
+
198
+ Two frontiers, and they are not the same:
199
+
200
+ - **The contract, the lint, and the claim join** reach every supported dialect.
201
+ `ts-vitest` reads `describe`/`it`/`test` titles out of `*.test.*` and `*.spec.*`
202
+ files. `swift` reads Swift Testing `@Test("…")` and `@Suite("…")` display names, and
203
+ XCTest `func test…()` identifiers, out of `*Tests.swift` files and anything under a
204
+ `Tests` directory.
205
+ - **The oracle-strength heatmap** reaches TypeScript. It needs a mutation tool that
206
+ attributes coverage per test: StrykerJS with `perTest` coverage analysis, plus
207
+ Istanbul `json-summary` coverage. Elsewhere the reports are simply missing, and the
208
+ heatmap degrades on its own.
209
+
210
+ Adding a language is a Speccle change with tests behind it, never a regex a repo
211
+ supplies.
212
+
213
+ ## Repo facts
214
+
215
+ Two things Speccle needs are true about a repo, not decidable by Speccle: which **test
216
+ dialect** the repo is on, and the **suite command** that runs its tests — the checks gate
217
+ cannot infer `xcodebuild test -scheme Ladder`. They live in `.speccle/config.json` at the
218
+ repo root:
219
+
220
+ ```json
221
+ {
222
+ "dialect": "swift",
223
+ "suite": "xcodebuild test -scheme Ladder",
224
+ "overrides": [{ "path": "web", "dialect": "ts-vitest", "suite": "pnpm test" }]
225
+ }
226
+ ```
227
+
228
+ `speccle init` auto-detects the dialect — `swift` from a `Package.swift`, `ts-vitest`
229
+ otherwise — and a default suite command, then **writes the result down**: the written
230
+ record is the source of truth, never the detection, and `init` never overwrites a config
231
+ it finds. A mixed-language tree corrects the defaults under a subtree with `overrides`,
232
+ where the longest matching path wins
233
+ (ADR-0040).
234
+
235
+ This is the one thing Speccle reads from a repo, and it never grows into a knob on
236
+ judgement. "Speccle has no configuration" is really **"Speccle has no configurable
237
+ judgement"**: a dialect name or a shell command is a fact — Speccle cannot know it and
238
+ there is nothing to game — while a lint rule or the claim join decides whether you pass,
239
+ and stays fixed so a clean run keeps meaning the same thing in every repo.
@@ -0,0 +1,184 @@
1
+ ---
2
+ name: strengthen
3
+ description: Evaluate how well a feature's tests defend its code — check report freshness first, hand the human the exact coverage and mutation commands when reports are stale or missing, then read the per-criterion oracle-strength heatmap and route every surviving mutant to a killing test or a sharper criterion, marking the heatmap evaluated when done. Use when the user wants to strengthen tests, check oracle strength, asks whether tests would catch a bug, wants to kill surviving mutants, or says "strengthen this", "how well is this defended", "run the heatmap".
4
+ allowed-tools: Read(/${CLAUDE_PLUGIN_ROOT}/skills/*/references/**)
5
+ ---
6
+
7
+ # strengthen
8
+
9
+ A slice can be green and badly defended: `implement-feature` leaves it that way on
10
+ purpose, and the `feature` pipeline's checks-gate never measures it. This skill is the
11
+ periodic audit that closes the gap — catching what no deterministic check sees:
12
+ behaviour no criterion covers, and tests that run without asserting.
13
+
14
+ The division of labour is fixed: **the human runs the expensive commands; this skill
15
+ evaluates what they produce.** Coverage and mutation are minutes of compute — never
16
+ run them yourself; hand over the exact commands and stop. Everything this skill runs
17
+ itself finishes in seconds.
18
+
19
+ Measure oracle strength per criterion, then act on every **surviving mutant** — the
20
+ exact code change no test noticed. The routing call is the whole job; §5 is how to
21
+ make it.
22
+
23
+ Speccle's words are mandatory: "oracle strength", not "mutation score"; "surviving
24
+ mutant", not "missed mutation"; "test-fitting", not "gaming the score"; "fresh" /
25
+ "stale" / "evaluated" as the freshness check reports them.
26
+
27
+ ## 1. Resolve the oracle
28
+
29
+ Once, in this order, and reuse what works:
30
+
31
+ 1. `<repo-root>/node_modules/.bin/speccle` — the repo's own pinned copy, put
32
+ there by `strength init`. A devDependency is never on `PATH` in this shell, so test
33
+ for the file; in a monorepo check the package you are working in as well as the
34
+ root. It wins over a global install: the pin is a committed choice, and lint rules
35
+ change between versions.
36
+ 2. `speccle` on `PATH` — a global install.
37
+ 3. Otherwise, from a clone of the speccle repo, run it from source — Node ≥ 24 executes
38
+ TypeScript directly, so no build is needed:
39
+ `node <speccle-repo>/packages/oracle/src/cli.ts`.
40
+
41
+ If none resolves, point the user at the install steps in Speccle's README and stop.
42
+
43
+ ## 2. Check freshness before anything else
44
+
45
+ ```sh
46
+ <oracle> strength <path> --check
47
+ ```
48
+
49
+ Seconds, no reports run: each report comes back **fresh**, **stale** (naming the
50
+ slice file that post-dates it), or **missing**, plus whether the current heatmap was
51
+ already **evaluated** and the marker path that records it. `--json` gives
52
+ `{ root, mutation, coverage, evaluated, marker }`; pass `--mutation` / `--coverage`
53
+ when the target writes reports somewhere non-default. Exit `0` means both fresh.
54
+
55
+ Route on what it says:
56
+
57
+ - **Fresh and not evaluated** — there is a heatmap waiting to be read. Go to §4.
58
+ - **Fresh and evaluated** — this heatmap has already been worked. Say so and stop;
59
+ re-evaluate only if the human asks for a second pass.
60
+ - **Stale or missing** — hand the human the exact commands and stop. Name the
61
+ target's own coverage and mutation commands (read its `package.json` scripts), and
62
+ when the work concerns one slice, scope the mutation run to it — mutating
63
+ `features/<name>/src/**` is minutes where the whole target is tens of them:
64
+
65
+ ```sh
66
+ pnpm coverage
67
+ pnpm mutation -- --mutate 'features/basket/src/**/*.ts'
68
+ ```
69
+
70
+ The run resumes when the human says the reports are in — re-run `--check` to
71
+ confirm rather than taking it on faith.
72
+
73
+ ## 3. Require the stack, and never install it silently
74
+
75
+ Oracle strength needs to know which tests covered each mutant, and that constrains the
76
+ target to TypeScript + vitest + StrykerJS. When `--check` says missing and the stack
77
+ itself is absent, check the four requirements in
78
+ `${CLAUDE_SKILL_DIR}/references/stack.md` before asking anyone to run anything.
79
+
80
+ **If something is missing, stop and offer.** Show what is absent, then offer the one
81
+ command that provisions all of it — `speccle strength init <path>` — and wait for
82
+ the user's go-ahead before running it. Never write to the target's `package.json`,
83
+ lockfile, or test config without the user agreeing first — this skill measures someone
84
+ else's project; it does not quietly re-tool it.
85
+
86
+ ## 4. Read the heatmap
87
+
88
+ ```sh
89
+ <oracle> strength <path>
90
+ ```
91
+
92
+ The reports are already on disk — §2 proved it. Show the human heatmap to the user: a
93
+ bar and `killed/covered` per criterion, each criterion's survivors listed beneath it. Use
94
+ `--json` for the routing work and never scrape the human output — the output shape and
95
+ the full `--json` schema are in `${CLAUDE_SKILL_DIR}/references/heatmap.md`. `strength`
96
+ is a report, not a gate: it exits `0` with survivors present, `2` on a bad or missing
97
+ report.
98
+
99
+ Four of the `--json` fields — `unclaimed`, `unknownClaims`, `unclaimedMutants`,
100
+ `staticMutants` — are **not** routing work: none is a survivor to route. Say them out
101
+ loud before you start; `references/heatmap.md` gives each one's shape and what it asks of
102
+ you instead.
103
+
104
+ ## 5. Route every surviving mutant
105
+
106
+ Never route a criterion, and never route a percentage — a criterion under 100% always has
107
+ at least one survivor to act on, and one at 100% has none, so the number carries nothing
108
+ the survivor list does not.
109
+ Route each survivor, and ask one question of it:
110
+
111
+ > Can I write an assertion that **follows from the criterion's statement**, and that fails
112
+ > against this mutant?
113
+
114
+ **Machine path — yes.** The statement already promises the behaviour this mutant breaks;
115
+ the suite just never checked it. Write the killing test under that criterion id, exactly
116
+ as `implement-feature` does — the `[KEY-n]` token in the full concatenated name. Run the
117
+ test suite — it must be green — and batch the machine-path work across survivors: each
118
+ killing test makes the reports staler, and only the human's re-run confirms the kills.
119
+
120
+ **Human path — no, and the behaviour matters.** No criterion entails it, so the spec is
121
+ silent or too vague to test. Draft a sharper statement, or the new criterion the spec
122
+ owes, taking the next never-used number under the key. Draft it in the target's `SPEC.md`
123
+ itself — the spec edit _is_ the draft, and lint reads the real file. Lint it clean,
124
+ **announce it** — id and statement — and keep going: the survivor takes the machine path
125
+ under the new id.
126
+ The human rules on every criterion this run added at the summary in §6; an overruled
127
+ one is reverted from the spec along with its test, returning its survivor to the report.
128
+
129
+ **Equivalent mutant — no, and no test ever could.** The mutated code means the same thing.
130
+ Annotate it where it lives and move on:
131
+
132
+ ```ts
133
+ // Stryker disable next-line ArithmeticOperator: x * 1 and x / 1 are the same value
134
+ ```
135
+
136
+ Argue it from the semantics. "The test is awkward to write" is not equivalence, and
137
+ reaching for this exit more than rarely means the answer was the machine or human path.
138
+
139
+ ### The trap
140
+
141
+ `[CHECKOUT-3]` above says _"checkout rejects it"_ of an oversized basket. Its two
142
+ survivors are the error's message string and `this.name`; the test asserts
143
+ `toThrow(TooManyLineItems)`, so `instanceof` reads neither.
144
+
145
+ Asserting the message text under `[CHECKOUT-3]` kills both mutants and turns the bar
146
+ green. Do not. "Rejects" promises a refusal and says nothing about what the refusal
147
+ _says_ — the test would pass because someone typed that string, and the next person to
148
+ reword the message gets a failure that teaches them nothing. That is test-fitting the
149
+ mutant, and it is how mutation testing rots into a score chase.
150
+
151
+ These survivors are the human path. The spec owes a criterion about what the rejection
152
+ tells the caller. Draft it, announce it, test it — and watch `[CHECKOUT-3]` reach 100%
153
+ untouched, because a kill counts for every criterion covering the mutant.
154
+
155
+ If oracle strength rose but the spec did not change, check that you did not fit a test to
156
+ a mutant.
157
+
158
+ ## 6. Confirm, mark evaluated, hand back
159
+
160
+ Confirmation needs fresh evidence, and producing it is the human's run: after your
161
+ last edit, hand back the same commands from §2 and stop. When the new reports land,
162
+ `--check` again, re-read the heatmap, and verify every machine-path survivor is gone.
163
+ A heatmap from before the change is not evidence.
164
+
165
+ When the evaluation is done — routed, confirmed, or explicitly parked — touch the
166
+ marker `--check` named, so the next run knows this heatmap was read:
167
+
168
+ ```sh
169
+ touch <marker-path>
170
+ ```
171
+
172
+ Then report, verified rather than assumed:
173
+
174
+ 1. The test suite is green.
175
+ 2. Every survivor you took the machine path on is gone from the fresh report.
176
+ 3. Oracle strength, before and after, headline and per criterion.
177
+ 4. The **spec summary**: every criterion this run added to the spec, id and statement.
178
+ The human rules here — an overruled criterion is reverted along with its killing
179
+ test, returning its survivor to the report.
180
+ 5. What is left, and why: each remaining survivor is annotated as equivalent, with its
181
+ argument, or named as a gap awaiting the human's ruling.
182
+
183
+ A survivor you cannot explain is not finished — say so plainly rather than letting a
184
+ nicer headline imply otherwise.
@@ -0,0 +1,39 @@
1
+ <!-- Generated from docs/strength-heatmap.md by scripts/sync-plugin-references.mjs — do not edit.
2
+ Edit the source and run `pnpm sync:plugin-refs`. -->
3
+
4
+ # The oracle-strength heatmap
5
+
6
+ `<oracle> strength <path>` prints a bar and `killed/covered` per criterion, with each
7
+ criterion's survivors listed beneath it:
8
+
9
+ ```
10
+ features/checkout/SPEC.md
11
+ CHECKOUT-1 ████████████████████ 100.0% 14/14 When a line item is taxed, tax rounds half-up to 2dp
12
+ CHECKOUT-3 ██████████████████░░ 88.2% 15/17 When a basket exceeds 100 line items, checkout rejects it
13
+ features/checkout/checkout.ts:13:11 StringLiteral → ``
14
+ features/checkout/checkout.ts:14:17 StringLiteral → ""
15
+ ```
16
+
17
+ Use `--json` for the routing work: `{ root, strength, lineCoverage, features[], unclaimed,
18
+ unknownClaims, unclaimedMutants, staticMutants }`, each criterion carrying `survivors[]`
19
+ with `file`, `line`, `column`, `mutator`, `replacement`. `strength` is a report, not a
20
+ gate: it exits `0` with survivors present, `2` on a bad or missing report. Never scrape
21
+ the human output.
22
+
23
+ ## The four fields that are not routing work
24
+
25
+ Say these out loud before you start — none of them is a survivor to route.
26
+
27
+ - **`unclaimed`** — a criterion no test's name carries. It scores nothing rather than
28
+ zero. It needs a test written against it before it can be weak; that is
29
+ `implement-feature`'s job, not a survivor to route.
30
+ - **`unknownClaims`** — a test claims an id no spec declares. Someone renamed or deleted a
31
+ criterion. Fix the test name or restore the criterion.
32
+ - **`unclaimedMutants`** — code the criteria do not reach at all, each entry naming its
33
+ `file`, `line`, `column`, `mutator`, `replacement`. Not weak criteria; a map of the
34
+ regions the spec is silent about. Do not route these as survivors — name the region to
35
+ the human and let them decide whether a feature owes a spec there.
36
+ - **`staticMutants`** — mutants that run at module load (word lists, regex literals), so
37
+ per-test coverage can attribute them to no criterion: `{ killed, survivors[] }`. The
38
+ killed ones are fine — some test noticed. A survivor is a real gap, but it can never be
39
+ claimed by a criterion id; name it to the human alongside the unclaimed mutants.
@@ -0,0 +1,23 @@
1
+ <!-- Generated from docs/strength-stack.md by scripts/sync-plugin-references.mjs — do not edit.
2
+ Edit the source and run `pnpm sync:plugin-refs`. -->
3
+
4
+ # The oracle-strength stack
5
+
6
+ Oracle strength needs to know which tests covered each mutant, and that constrains the
7
+ target to TypeScript + vitest + StrykerJS. When `--check` says missing and the stack
8
+ itself is absent, check for all four before asking anyone to run anything:
9
+
10
+ | Requirement | Where |
11
+ | --------------------------------- | ------------------------------------ |
12
+ | `@stryker-mutator/core` + runner | `package.json` devDependencies |
13
+ | `coverageAnalysis: "perTest"` | `stryker.config.json` |
14
+ | the `json` reporter, and its path | `stryker.config.json` `jsonReporter` |
15
+ | istanbul provider, `json-summary` | `vitest.config.ts` `test.coverage` |
16
+
17
+ `perTest` is the hard one: without it Stryker never records `coveredBy`, the join has
18
+ nothing to walk back to criterion ids, and `oracle strength` exits `2` saying so.
19
+
20
+ `speccle strength init <path>` provisions all of it — it installs the missing
21
+ devDependencies and writes the preset configs, keeping any that already exist. It runs
22
+ only on the user's explicit go-ahead; this skill measures someone else's project, it does
23
+ not quietly re-tool it.