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.
- package/README.md +10 -1
- package/dist/claims.js +5 -1
- package/dist/cli.js +118 -3
- package/dist/config.js +114 -0
- package/dist/doctor.js +127 -0
- package/dist/init.js +5 -4
- package/dist/render.js +106 -0
- package/dist/skills.js +63 -0
- package/dist/update.js +39 -0
- package/package.json +4 -3
- package/skills/carve-feature/SKILL.md +183 -0
- package/skills/carve-feature/references/convention.md +239 -0
- package/skills/conform/SKILL.md +129 -0
- package/skills/conform/references/convention.md +239 -0
- package/skills/feature/SKILL.md +103 -0
- package/skills/implement-feature/SKILL.md +152 -0
- package/skills/implement-feature/references/convention.md +239 -0
- package/skills/plan-feature/SKILL.md +125 -0
- package/skills/plan-feature/references/convention.md +239 -0
- package/skills/spec-feature/SKILL.md +163 -0
- package/skills/spec-feature/references/convention.md +239 -0
- package/skills/strengthen/SKILL.md +184 -0
- package/skills/strengthen/references/heatmap.md +39 -0
- package/skills/strengthen/references/stack.md +23 -0
|
@@ -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,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-feature
|
|
3
|
+
description: Shape a feature request into a ratified plan — take prose, a ticket, or a conversation as it comes, explore the repo, route the work (a new slice, an amendment to the slice that already owns the behaviour, or a carve), settle every open key decision with the user one question at a time, capture each one into the slice's docs as it lands, and end with an easy-to-read plan summary. Use when the user wants to plan or scope a feature before building it, asks where a behaviour should live, wants to agree open decisions about a feature, or asks whether something is a new feature or a change to an existing one.
|
|
4
|
+
allowed-tools: Read(/${CLAUDE_PLUGIN_ROOT}/skills/*/references/**)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# plan-feature
|
|
8
|
+
|
|
9
|
+
Turn a feature request — in whatever form it arrives — into a plan the rest of the
|
|
10
|
+
pipeline can execute unattended: the **route**, the **feature folder**, the **feature
|
|
11
|
+
key**, the scope, and every open decision settled and written down. This is stage 1
|
|
12
|
+
of the `feature` pipeline and its one human gate; it is also useful alone — a plan is
|
|
13
|
+
a cheap thing to be wrong about out loud.
|
|
14
|
+
|
|
15
|
+
The folder shape and key rules this skill plans against are fixed by the convention,
|
|
16
|
+
bundled beside this skill. Read `${CLAUDE_SKILL_DIR}/references/convention.md` before
|
|
17
|
+
shaping anything.
|
|
18
|
+
|
|
19
|
+
Speccle's words are fixed and mandatory: "amend", not "edit" or "update"; "feature
|
|
20
|
+
key", not "prefix"; "criterion id", not "tag"; "key decision" and "defaulted" as
|
|
21
|
+
defined here.
|
|
22
|
+
|
|
23
|
+
## 1. Take the input as it comes
|
|
24
|
+
|
|
25
|
+
Prose, a ticket, a scratch file, a conversation, or a `SPEC.md` someone already wrote
|
|
26
|
+
to the convention. Never send the user away to reformat something first. If the input
|
|
27
|
+
is already a conventioned `SPEC.md`, the criteria are owned — the plan adopts them
|
|
28
|
+
as-is and routes on where that spec should live.
|
|
29
|
+
|
|
30
|
+
## 2. Explore before routing
|
|
31
|
+
|
|
32
|
+
The route is decided by **where the behaviour lives**, and finding that out means
|
|
33
|
+
looking:
|
|
34
|
+
|
|
35
|
+
- Find every existing feature folder — every `SPEC.md` — and read each one's key and
|
|
36
|
+
criteria statements. This is also the key-collision check, so it is never wasted.
|
|
37
|
+
- Read the language: the owning slice's `CONTEXT.md` and the project's root glossary,
|
|
38
|
+
where they exist. A request that uses a term those files define differently is
|
|
39
|
+
challenged out loud — "your glossary defines X as …, but you seem to mean Y" —
|
|
40
|
+
never silently translated.
|
|
41
|
+
- Read the project's layout: where feature folders sit, what the test runner is, how
|
|
42
|
+
the slice would be reached from the outside.
|
|
43
|
+
|
|
44
|
+
Then route:
|
|
45
|
+
|
|
46
|
+
- **New** — no governed slice owns the behaviour. The plan names a new feature folder
|
|
47
|
+
and key.
|
|
48
|
+
- **Amend** — a governed slice already owns it. Extending (new criteria) and changing
|
|
49
|
+
(rewording or retiring criteria) are the same route: the plan names the owning
|
|
50
|
+
folder and its existing key. Never plan a second folder for behaviour that has an
|
|
51
|
+
owner.
|
|
52
|
+
- **Carve** — the behaviour already runs but is ungoverned. Stop and hand to
|
|
53
|
+
`carve-feature`; a request that mixes governing and changing is a carve followed by
|
|
54
|
+
an amend, never one pass.
|
|
55
|
+
|
|
56
|
+
When the call is genuinely close — the behaviour half-belongs to an existing slice —
|
|
57
|
+
route **amend** and say why: two slices owning one behaviour is the expensive mistake,
|
|
58
|
+
and a wrongly-amended slice is cheap to split later.
|
|
59
|
+
|
|
60
|
+
## 3. Shape the slice
|
|
61
|
+
|
|
62
|
+
- **Where the feature folder goes** (new route). Match the project's existing layout;
|
|
63
|
+
if other feature folders exist, sit beside them. The folder is **named for the
|
|
64
|
+
feature** — never an unnamed catch-all like `src/` or `lib/` — even when it is the
|
|
65
|
+
project's first.
|
|
66
|
+
- **The feature key.** New route: `[A-Z][A-Z0-9]{1,9}`, unique across the repo — you
|
|
67
|
+
already read every other spec's frontmatter in phase 2. Amend route: the slice's
|
|
68
|
+
existing key, never a new one.
|
|
69
|
+
- **The slice's scope.** Name the behaviours the spec will cover, in a sentence or
|
|
70
|
+
two each — what is in, and what is deliberately out. These are raw material for
|
|
71
|
+
`spec-feature`, not criteria: leave the statements to the drafting.
|
|
72
|
+
|
|
73
|
+
## 4. Settle the open decisions — one question at a time
|
|
74
|
+
|
|
75
|
+
Planning is where open choices get agreed, not guessed. A **key decision** is a choice
|
|
76
|
+
the input leaves open, with more than one viable answer, that materially shapes the
|
|
77
|
+
slice across criteria — a policy (rounding, retention, who may retry), a data shape,
|
|
78
|
+
an external contract.
|
|
79
|
+
|
|
80
|
+
- **Ask only what changes the plan.** A choice the ticket or conversation already
|
|
81
|
+
settles is not open — adopt it. Routing is never a key decision: it is decided by
|
|
82
|
+
where the behaviour lives and announced. Naming, statement wording, and
|
|
83
|
+
single-behaviour details belong to `spec-feature` and the criterion body. If
|
|
84
|
+
nothing is genuinely open, say "no open decisions" and move on — never manufacture
|
|
85
|
+
questions to look thorough.
|
|
86
|
+
- **One at a time, each with a recommendation.** Put the options, recommend one, and
|
|
87
|
+
say why — then wait. The next question may depend on the last answer; a batch
|
|
88
|
+
cannot. This dialogue is the reason planning runs in the main session and not a
|
|
89
|
+
subagent.
|
|
90
|
+
- **If no one can answer** — an unattended run — take the recommendation and flag the
|
|
91
|
+
decision as **defaulted**, in the plan summary and again at the end of the
|
|
92
|
+
pipeline. A defaulted decision is never silent.
|
|
93
|
+
|
|
94
|
+
## 5. Capture each decision the moment it lands
|
|
95
|
+
|
|
96
|
+
A decision that lives only in this conversation dies with it. Write it now, not at
|
|
97
|
+
the end:
|
|
98
|
+
|
|
99
|
+
- **Feature-level** — an ADR in the slice's `decisions/` (next number, slug, the
|
|
100
|
+
usual status/context/decision/consequences) and, when it coins or sharpens a term,
|
|
101
|
+
an entry in the slice's `CONTEXT.md` with its _Avoid_ line. On the new route the
|
|
102
|
+
feature folder exists from the first captured decision — create it, named per the
|
|
103
|
+
convention; `spec-feature` fills in the rest of the contract later.
|
|
104
|
+
- **Repo-wide** — a decision bigger than the slice goes to the project's own ADR
|
|
105
|
+
home (`docs/adr/` by convention) and its root glossary, in whatever form the
|
|
106
|
+
project already uses.
|
|
107
|
+
- **About one behaviour** — no file yet; note it in the plan for `spec-feature` to
|
|
108
|
+
land in that criterion's body.
|
|
109
|
+
- **When writes are forbidden** — the session is in plan mode — carry every capture
|
|
110
|
+
in the plan summary, explicitly marked "to write on approval". Writing them is the
|
|
111
|
+
pipeline's first act after the gate.
|
|
112
|
+
|
|
113
|
+
## 6. End with the plan summary
|
|
114
|
+
|
|
115
|
+
One screen, easy to read: the route, the feature folder, the key, the scope (in and
|
|
116
|
+
out), and each key decision — how it was settled (from the input, agreed, or
|
|
117
|
+
defaulted) and where it was captured. No jargon the reader has to decode; the summary
|
|
118
|
+
is the thing the human approves, so it must be readable in one pass.
|
|
119
|
+
|
|
120
|
+
In the `feature` pipeline the summary becomes the approval gate — the orchestrator
|
|
121
|
+
owns those mechanics. Standalone, hand it back and stop: the plan is the deliverable.
|
|
122
|
+
|
|
123
|
+
This skill drafts no criteria and writes no code — its only files are the decision
|
|
124
|
+
records above. A plan that turns out wrong at the spec or implement stage is revised
|
|
125
|
+
there, not defended here.
|
|
@@ -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.
|