speccle 0.11.0 → 0.15.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 +290 -3
- package/dist/calibration.js +156 -0
- package/dist/changeset.js +141 -0
- package/dist/claims.js +5 -1
- package/dist/cli.js +596 -4
- package/dist/config.js +117 -0
- package/dist/doctor.js +141 -0
- package/dist/init.js +5 -4
- package/dist/lenses.js +68 -0
- package/dist/remedy.js +133 -0
- package/dist/render.js +323 -2
- package/dist/reviewinit.js +139 -0
- package/dist/reviewrun.js +516 -0
- package/dist/risk.js +228 -0
- package/dist/skills.js +63 -0
- package/dist/update.js +48 -0
- package/dist/verify.js +92 -0
- package/lenses/accessibility.md +39 -0
- package/lenses/architecture.md +38 -0
- package/lenses/correctness.md +36 -0
- package/lenses/house-conventions.md +37 -0
- package/lenses/performance.md +40 -0
- package/lenses/risk.md +45 -0
- package/lenses/security.md +42 -0
- package/lenses/test-quality.md +39 -0
- package/package.json +7 -5
- 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/review/SKILL.md +190 -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,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review
|
|
3
|
+
description: Review a change set the way the outer loop does — fan a panel of lenses over the working diff, let `oracle risk` decide whether a human is needed, then below that threshold fix what the lenses find (re-running the checks-gate after each fix and reverting any that goes red) and above it report findings and stop, closing with an overruleable summary that proposes a remedy for each finding — recalled from and recorded to the remedy record — and records the change to the calibration record. Use when the user wants to review a branch or the pending change, asks to "review this", "review my changes", "run the lenses", find-and-fix issues before a PR, or check a change set for correctness, security, accessibility, architecture, performance, test-quality, or house-convention problems.
|
|
4
|
+
allowed-tools: Read(/${CLAUDE_PLUGIN_ROOT}/skills/*/references/**)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# review
|
|
8
|
+
|
|
9
|
+
The outer loop. Its unit is the **change set** — the branch or PR's pending change,
|
|
10
|
+
not a slice — which is why it is separate from `feature`. It fans a panel of **lenses**
|
|
11
|
+
over the working diff, and — gated by the change's **risk score** — either fixes what it
|
|
12
|
+
finds or reports and stops for a human. It generates feedback; it does not itself change
|
|
13
|
+
the inner loop.
|
|
14
|
+
|
|
15
|
+
Speccle's words are fixed and mandatory: "change set", not "diff" or "PR"; "lens", not
|
|
16
|
+
"reviewer" or "rule"; "finding", not "issue" or "comment"; "remedy", not "fix" alone;
|
|
17
|
+
"risk score" / "review threshold" / "floor"; "escalate", never "de-escalate". Speccle's
|
|
18
|
+
posture is **announce, never gate**: below the threshold the fixes just land and the
|
|
19
|
+
summary reports them; there is no "apply? y/n".
|
|
20
|
+
|
|
21
|
+
## 1. Resolve the oracle
|
|
22
|
+
|
|
23
|
+
Once, in this order, and reuse what works:
|
|
24
|
+
|
|
25
|
+
1. `<repo-root>/node_modules/.bin/speccle` — the repo's own pinned copy. A devDependency
|
|
26
|
+
is never on `PATH` in this shell, so test for the file; in a monorepo check the package
|
|
27
|
+
you are working in as well as the root. It wins over a global install: the pin is a
|
|
28
|
+
committed choice, and rules change between versions.
|
|
29
|
+
2. `speccle` on `PATH` — a global install.
|
|
30
|
+
3. Otherwise, from a clone of the speccle repo, run it from source — Node ≥ 24 executes
|
|
31
|
+
TypeScript directly: `node <speccle-repo>/packages/oracle/src/cli.ts`.
|
|
32
|
+
|
|
33
|
+
If none resolves, point the user at the install steps in Speccle's README and stop. If
|
|
34
|
+
`.speccle/lenses/` is missing, the repo was never initialized for review — offer
|
|
35
|
+
`speccle init` and stop.
|
|
36
|
+
|
|
37
|
+
## 2. The change set
|
|
38
|
+
|
|
39
|
+
Everything here runs over the same change set: the working tree's pending change against
|
|
40
|
+
its last commit. Read it once — `git status --porcelain --untracked-files=all` for the
|
|
41
|
+
files, `git diff HEAD` for the lines — and hand the changed files and their diff to every
|
|
42
|
+
lens. A finding must anchor to a **changed line**; a lens does not audit the whole repo.
|
|
43
|
+
|
|
44
|
+
If the working tree is clean, there is nothing to review — say so and stop.
|
|
45
|
+
|
|
46
|
+
## 3. Risk — the authority gate, before any fixing
|
|
47
|
+
|
|
48
|
+
Fix authority is not a global setting; it depends on the change. Decide it first, because
|
|
49
|
+
it decides whether the panel may fix at all.
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
<oracle> risk <path> --json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This returns the deterministic **floor**: `score`, `threshold`, `humanRequired`, and the
|
|
56
|
+
spec-aware `signals` that fired, each with its evidence. Then apply the **risk lens**
|
|
57
|
+
(`.speccle/lenses/risk.md`) yourself: read the diff for the consequential change no signal
|
|
58
|
+
could see, and emit its one escalation decision. The lens may **raise** the floor or
|
|
59
|
+
require a human outright; it may **never lower** either. A concern that the floor is too
|
|
60
|
+
high goes in the summary for the human — you do not act on it.
|
|
61
|
+
|
|
62
|
+
The verdict a human is required when **any** of these holds: `risk.humanRequired` is true,
|
|
63
|
+
the risk lens set `requireHuman`, or `score + raiseFloorBy ≥ threshold`.
|
|
64
|
+
|
|
65
|
+
- **Below the threshold, no escalation** → `review` may fix unasked (§5).
|
|
66
|
+
- **At or above, or the lens escalated** → `review` still runs the panel to report, but
|
|
67
|
+
**fixes nothing**; a human is required.
|
|
68
|
+
|
|
69
|
+
Both the computed floor and any escalation must be legible in the summary — a risk verdict
|
|
70
|
+
with no visible reasoning is not auditable.
|
|
71
|
+
|
|
72
|
+
## 4. The lens panel — fan out over the diff
|
|
73
|
+
|
|
74
|
+
Fan out one subagent per lens in `.speccle/lenses/*.md`, in parallel — this is the local
|
|
75
|
+
driver, so the panel runs as subagents in this session, no API key. Skip two:
|
|
76
|
+
|
|
77
|
+
- `risk.md` — already applied in §3; it escalates authority, it is not a finding lens.
|
|
78
|
+
- `house-conventions.md` **when it still reads as the shipped template** — its body carries
|
|
79
|
+
the `speccle:lens-template` marker until a repo authors it. An unauthored lens is inert;
|
|
80
|
+
skip it and note that the repo has not written its house-conventions lens yet.
|
|
81
|
+
|
|
82
|
+
Each subagent's prompt carries the lens file verbatim (its stance, what to look for, how to
|
|
83
|
+
report), the changed files and their diff, and the instruction to return findings anchored
|
|
84
|
+
to changed lines only, in the shape the lens describes: `path:line`, severity, what, why,
|
|
85
|
+
the fix, and a remedy route. A lens that finds nothing returns an empty list — the common,
|
|
86
|
+
valid result. Collect every finding across the panel.
|
|
87
|
+
|
|
88
|
+
## 5. Fix — only with authority, and never without the checks-gate
|
|
89
|
+
|
|
90
|
+
If §3 said a human is required, **fix nothing**; go to §6 and report.
|
|
91
|
+
|
|
92
|
+
Otherwise fix each finding, smallest change first, and guard every one:
|
|
93
|
+
|
|
94
|
+
1. Apply the fix the finding names.
|
|
95
|
+
2. **Re-run the checks-gate** — resolve the same oracle and run `<oracle> lint` on any
|
|
96
|
+
governed slice the fix touched, `<oracle> claims <root>`, `<oracle> verify <root>` (the
|
|
97
|
+
cross-file invariants), and the project's own test suite.
|
|
98
|
+
3. **Green** — keep the fix. **Red** — **revert it, do not salvage**: restore the file to its
|
|
99
|
+
pre-fix state and carry the finding forward as unfixed. A fix that cannot pass the gate is
|
|
100
|
+
the human's call, not a thing to patch around.
|
|
101
|
+
|
|
102
|
+
Batch sensibly, but keep each fix independently revertible — a fix you cannot back out on
|
|
103
|
+
its own is one you cannot safely apply. The fixes join the working change set; `review` does
|
|
104
|
+
**not** commit — the change set is the human's or the inner loop's to land.
|
|
105
|
+
|
|
106
|
+
## 6. Route each finding to a remedy
|
|
107
|
+
|
|
108
|
+
For every finding — fixed, reverted, or left for the human — name the durable artefact that
|
|
109
|
+
stops the class recurring, routed on **what the finding is**, never on a count, the same
|
|
110
|
+
posture `strengthen` takes to a surviving mutant. The **remedy record** is the meta loop's
|
|
111
|
+
memory of that routing: consult it before deriving, add to it after applying.
|
|
112
|
+
|
|
113
|
+
**Recall first — answer a repeat the way you answered it before.** Give the finding a short,
|
|
114
|
+
stable class handle (kebab-case, e.g. `missing-model-roundtrip-test`) and ask the record:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
<oracle> remedy recall <path> --class <handle>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A hit is the known-correct remedy — reuse its route and artefact rather than re-deriving, so
|
|
121
|
+
the same finding gets the same answer. A miss means route it fresh:
|
|
122
|
+
|
|
123
|
+
- a deterministic, cross-file invariant → an `oracle verify` check in `.speccle/checks/`.
|
|
124
|
+
- behaviour a criterion should own → a new acceptance criterion, and its tagged test, in the
|
|
125
|
+
owning `SPEC.md`.
|
|
126
|
+
- a dimension a lens missed or stated too weakly → sharpen the lens in `.speccle/lenses/`,
|
|
127
|
+
the house-conventions lens above all.
|
|
128
|
+
- a genuine one-off → none.
|
|
129
|
+
|
|
130
|
+
**Then record what you applied**, so the next review recalls it:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
<oracle> remedy record <path> --class <handle> --finding <what> --fix <what you did> \
|
|
134
|
+
--route <check|criterion|lens|none> [--artefact <ref>]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`--artefact` names the prevention home — the `.speccle/checks/` or `.speccle/lenses/` path, or
|
|
138
|
+
the `SPEC.md` criterion id — and is required for every route but `none`. Record the remedies you
|
|
139
|
+
**applied** this run; a finding left for the human (§3 required one, so nothing was fixed) is
|
|
140
|
+
proposed in the summary, never recorded — the record holds enacted remedies, not intentions, the
|
|
141
|
+
same honesty §8 keeps for the verdict.
|
|
142
|
+
|
|
143
|
+
## 7. Summary — announce, never gate
|
|
144
|
+
|
|
145
|
+
Render one screen, in product voice. The human rules by reading it, not by being asked:
|
|
146
|
+
|
|
147
|
+
1. **The risk verdict** — `score` against `threshold`, the floor's fired signals with their
|
|
148
|
+
evidence, any risk-lens escalation with its reason, and whether a human was required.
|
|
149
|
+
2. **Every finding** — its lens, `path:line`, severity, what and why, and its outcome:
|
|
150
|
+
**fixed**, **reverted** (the checks-gate went red — say what failed), or **left for the
|
|
151
|
+
human** (a human was required, so nothing was fixed).
|
|
152
|
+
3. **The proposed remedy** per finding (§6).
|
|
153
|
+
4. **The checks-gate now** — green or red, named.
|
|
154
|
+
|
|
155
|
+
There is no approval gate. Below the threshold the fixes already landed and the human
|
|
156
|
+
reverts what the summary makes them regret; at or above it nothing was fixed and the findings
|
|
157
|
+
await the human. Either way the summary is the whole interaction — overruleable, never a
|
|
158
|
+
"proceed? y/n".
|
|
159
|
+
|
|
160
|
+
## 8. Calibrate — record the change, then surface the evidence
|
|
161
|
+
|
|
162
|
+
The review is the outer loop's eyes; the **calibration record** is its memory — one entry per
|
|
163
|
+
reviewed change, the evidence a **review threshold** earns a move on. Close every review by
|
|
164
|
+
adding this change to it.
|
|
165
|
+
|
|
166
|
+
The floor is known (§3) and the panel's outcome is known (§4). The one thing neither can
|
|
167
|
+
answer is the honest verdict — _did this change actually need a human?_ That answer is the
|
|
168
|
+
human's, and the record is worthless without it, so never invent it:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
<oracle> calibrate record <path> --found-real <true|false> --needed-human <true|false> [--escalated]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `--found-real` — true when any lens returned a real finding this run; you know this.
|
|
175
|
+
- `--escalated` — include it only if the risk lens escalated the floor in §3.
|
|
176
|
+
- `--needed-human` — the human's honest call, whatever the floor said. With a human here to
|
|
177
|
+
answer, ask once and record it; with none, print the command for them to run and stop. A
|
|
178
|
+
threshold that rose on a guessed verdict is worse than one that never rose.
|
|
179
|
+
|
|
180
|
+
Then read the record back and fold its proposals into the close:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
<oracle> calibrate report <path>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
It names the signals that have never fired on a change that mattered, the signals that fired
|
|
187
|
+
on every change that needed a human, and the threshold the record would support. These are
|
|
188
|
+
**evidence, not instructions**: escalation is free, but only a human moves a weight or the
|
|
189
|
+
review threshold, and only on this evidence. Surface them so ignoring them is a choice — never
|
|
190
|
+
act on them here.
|
|
@@ -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.
|