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,42 @@
|
|
|
1
|
+
# security lens
|
|
2
|
+
|
|
3
|
+
**Stance:** treat every input in the diff as hostile and every output as reachable by
|
|
4
|
+
someone it was not meant for. Trust nothing the change did not itself validate.
|
|
5
|
+
|
|
6
|
+
## What to look for
|
|
7
|
+
|
|
8
|
+
- **Injection** — user input concatenated into SQL, a shell command, an HTML fragment, a
|
|
9
|
+
file path, a regex, a template, or a log line. Look for the missing parameterization,
|
|
10
|
+
escape, or allow-list, not just the obvious `eval`.
|
|
11
|
+
- **AuthN / AuthZ** — an endpoint, action, or field newly reachable without the check its
|
|
12
|
+
neighbours have; an ownership check that trusts a client-supplied id; a role compared by
|
|
13
|
+
a mutable value.
|
|
14
|
+
- **Secrets** — a key, token, password, or connection string in the diff, a fixture, a log,
|
|
15
|
+
or an error message returned to the caller. Anything that should have come from the
|
|
16
|
+
environment and did not.
|
|
17
|
+
- **Sensitive data** — PII or credentials logged, cached, serialized into a response, or
|
|
18
|
+
widened into a payload that reaches a client that should not see it.
|
|
19
|
+
- **Untrusted deserialization / SSRF** — parsing attacker-controlled data into live objects;
|
|
20
|
+
a server-side fetch whose URL a caller controls.
|
|
21
|
+
- **Crypto & randomness** — a non-cryptographic RNG for a token, a home-rolled hash or
|
|
22
|
+
cipher, a comparison of secrets that is not constant-time, a disabled TLS verification.
|
|
23
|
+
- **Dependencies** — a new dependency, a pinned version dropped, a `postinstall` that runs
|
|
24
|
+
code, a lockfile change that does not match the manifest.
|
|
25
|
+
|
|
26
|
+
## How to report
|
|
27
|
+
|
|
28
|
+
Report only findings anchored to a **changed line** in this change set. For each finding
|
|
29
|
+
give:
|
|
30
|
+
|
|
31
|
+
- `path:line` — the changed line it anchors to
|
|
32
|
+
- **severity** — blocker · major · minor · nit (a reachable, unauthenticated data exposure is
|
|
33
|
+
a blocker; defence-in-depth is a minor)
|
|
34
|
+
- **what** — the weakness in one line
|
|
35
|
+
- **why** — the **attack**: who supplies what input, and what they gain
|
|
36
|
+
- **fix** — the parameterization, check, or removal that closes it
|
|
37
|
+
- **route** — `criterion` · `check` (e.g. "a handler under `api/` must call the authz guard")
|
|
38
|
+
· `lens` · `none`
|
|
39
|
+
|
|
40
|
+
Name the vector concretely — a payload, a request, an actor — not "could be exploited". A
|
|
41
|
+
theoretical concern with no reachable path is a nit; label it as one. An empty report is a
|
|
42
|
+
valid result.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# test-quality lens
|
|
2
|
+
|
|
3
|
+
**Stance:** a test's only job is to fail when the behaviour breaks. Read each test the change
|
|
4
|
+
adds or touches and ask what broken code it would let through.
|
|
5
|
+
|
|
6
|
+
## What to look for
|
|
7
|
+
|
|
8
|
+
- **Asserts nothing** — a test that exercises code but checks no outcome; an `expect` with no
|
|
9
|
+
matcher; a snapshot accepted without reading it; a `try/catch` that passes on either path.
|
|
10
|
+
- **Cannot fail** — an assertion tautologically true (`expect(x).toBe(x)`), a mock asserted
|
|
11
|
+
against its own return, a condition that guards the only assertion so it never runs.
|
|
12
|
+
- **Tests the mock** — every collaborator stubbed until the test only proves the stubs were
|
|
13
|
+
called in the order the test wrote; nothing of the real unit is left under test.
|
|
14
|
+
- **Behaviour changed, test did not** — production logic in the diff whose test file is
|
|
15
|
+
untouched, or touched only to keep it compiling; a new branch with no case.
|
|
16
|
+
- **Coupled to implementation** — assertions on private internals, call order, or log
|
|
17
|
+
strings rather than observable behaviour, so a safe refactor breaks the test and a real
|
|
18
|
+
regression slips by.
|
|
19
|
+
- **Weak oracle** — asserts a value is truthy where the exact value matters; `toThrow()`
|
|
20
|
+
with no error type; checks a collection's length but not its contents.
|
|
21
|
+
- **Missing negative space** — only the happy path; the error, the empty, and the rejected
|
|
22
|
+
input the change introduced go unchecked.
|
|
23
|
+
|
|
24
|
+
## How to report
|
|
25
|
+
|
|
26
|
+
Report only findings anchored to a **changed line** — a test in the diff, or production
|
|
27
|
+
behaviour the diff added that no test claims. For each finding give:
|
|
28
|
+
|
|
29
|
+
- `path:line` — the test line, or the unclaimed production line
|
|
30
|
+
- **severity** — major (a real regression would pass) · minor · nit
|
|
31
|
+
- **what** — the gap in one line
|
|
32
|
+
- **why** — the broken implementation this test would wave through
|
|
33
|
+
- **fix** — the assertion to add, or the case the suite owes
|
|
34
|
+
- **route** — `criterion` (behaviour the spec never named, so no test could claim it) ·
|
|
35
|
+
`check` · `lens` · `none`
|
|
36
|
+
|
|
37
|
+
Route to `criterion` when the gap is that nothing specifies the behaviour — a missing
|
|
38
|
+
assertion is a test fix, a missing specification is not. Do not reward a test that raises a
|
|
39
|
+
coverage number while asserting nothing. An empty report is a valid result.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speccle",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Speccle's deterministic tooling: spec lint and the oracle-strength heatmap.
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "Speccle's deterministic tooling: spec lint and the oracle-strength heatmap. No LLM, bar the opt-in CI review driver.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Matt Alton",
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
"speccle": "./dist/cli.js"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
|
-
"dist"
|
|
29
|
+
"dist",
|
|
30
|
+
"skills",
|
|
31
|
+
"lenses"
|
|
30
32
|
],
|
|
31
33
|
"scripts": {
|
|
32
|
-
"build": "tsc -p tsconfig.build.json",
|
|
33
|
-
"prepublishOnly": "pnpm build && pnpm test",
|
|
34
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/bundle-skills.mjs",
|
|
35
|
+
"prepublishOnly": "node ../../scripts/check-plugin-version.mjs --release && pnpm build && pnpm test",
|
|
34
36
|
"typecheck": "tsc --noEmit",
|
|
35
37
|
"test": "vitest run",
|
|
36
38
|
"coverage": "vitest run --coverage",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: carve-feature
|
|
3
|
+
description: Bring existing, ungoverned code under the convention without changing it — derive SPEC.md and CONTEXT.md from what the code observably does, lint them clean, announce the derived criteria and findings, then claim every criterion by tagging the tests that already defend it and writing tests for what nothing claims, ending with a spec summary the human rules on. Use when the user wants to retrofit a spec onto working code, govern an existing module, bring legacy code into a slice, or says "carve this", "carve it into a slice", "spec what this already does".
|
|
4
|
+
allowed-tools: Read(/${CLAUDE_PLUGIN_ROOT}/skills/*/references/**)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# carve-feature
|
|
8
|
+
|
|
9
|
+
Take a region of code that already works and make it a governed feature folder: the
|
|
10
|
+
markdown contract — `SPEC.md`, `CONTEXT.md`, `AGENTS.md`, `decisions/` — derived from
|
|
11
|
+
its observed behaviour, and every criterion claimed by a tagged test. The code's
|
|
12
|
+
behaviour does not change — a carve is a change of governance, not of code.
|
|
13
|
+
|
|
14
|
+
If the user is describing behaviour that does not exist yet, that is the `feature`
|
|
15
|
+
pipeline, not a carve. A request that mixes the two ("spec this up and fix the
|
|
16
|
+
rounding while you're in there") is a carve followed by governed work — never one
|
|
17
|
+
pass.
|
|
18
|
+
|
|
19
|
+
The shape of the folder is fixed by the convention, bundled beside this skill. Read
|
|
20
|
+
`${CLAUDE_SKILL_DIR}/references/convention.md` before deriving anything.
|
|
21
|
+
|
|
22
|
+
Speccle's words are fixed and mandatory: "criterion id", not "tag"; "statement", not
|
|
23
|
+
"title"; "carve", not "retrofit" or "migration"; "spec summary", not "approval gate".
|
|
24
|
+
|
|
25
|
+
**This skill does not measure oracle strength.** A carve can finish well-specified and
|
|
26
|
+
weakly defended; a fresh carve is `strengthen`'s natural next target, and saying so is
|
|
27
|
+
part of handing back.
|
|
28
|
+
|
|
29
|
+
## 1. Pick the carve boundary
|
|
30
|
+
|
|
31
|
+
The boundary is the directory that owns the code being carved; the markdown contract
|
|
32
|
+
lands at its root. A carve never moves or edits source files, so the boundary must
|
|
33
|
+
already have the convention's shape — where it does not, the fix is a **pre-carve
|
|
34
|
+
refactor** the user does before carving, under whatever safety net they trust. Stop
|
|
35
|
+
and show them exactly what is wrong; do not do it for them.
|
|
36
|
+
|
|
37
|
+
- **The boundary is named for the feature.** An unnamed catch-all (`src/`, `lib/`) is
|
|
38
|
+
never a feature folder, even when it holds the project's only feature — renaming it
|
|
39
|
+
is the pre-carve refactor case, not a valid boundary.
|
|
40
|
+
- **Source must already sit in the boundary's `src/`.** Source loose at the boundary
|
|
41
|
+
root, or smeared across the tree, is the same case: show the user which files live
|
|
42
|
+
where and what the shape should be.
|
|
43
|
+
- **Tests may live outside it** — a parallel `test/` tree is normal in existing
|
|
44
|
+
projects. Tests are the one thing a carve does move: they land in `src/` beside the
|
|
45
|
+
code in phase 6.
|
|
46
|
+
|
|
47
|
+
Settle the feature key as `plan-feature` does: `[A-Z][A-Z0-9]{1,9}`, unique across
|
|
48
|
+
the repo — read the frontmatter of every other `SPEC.md` before guessing.
|
|
49
|
+
|
|
50
|
+
If a conventioned `SPEC.md` already sits at the root, the criteria are already owned:
|
|
51
|
+
adopt them unchanged and go straight to phase 6 for whatever they leave unclaimed.
|
|
52
|
+
|
|
53
|
+
## 2. Derive the criteria from what is, not what should be
|
|
54
|
+
|
|
55
|
+
Read in this order, because it is the order of reliability:
|
|
56
|
+
|
|
57
|
+
1. **The existing tests** — executable claims about behaviour someone once cared about.
|
|
58
|
+
2. **The source** — for observable behaviour no test asserts.
|
|
59
|
+
3. **Docs and comments last** — they drift, and a carve documents the code, not the
|
|
60
|
+
folklore around it.
|
|
61
|
+
|
|
62
|
+
Every criterion is one testable clause the code **verifiably does today**. If you
|
|
63
|
+
cannot point at the code that makes a statement true, it is not observed behaviour and
|
|
64
|
+
it does not go in the spec. Criteria describe the feature at its boundary — what a
|
|
65
|
+
caller can see — not one per function; a spec that mirrors the file listing is an
|
|
66
|
+
implementation inventory, not acceptance criteria.
|
|
67
|
+
|
|
68
|
+
**When something looks wrong, do not write the criterion you wish were true.** The
|
|
69
|
+
code's spec fails against the code — that is a broken carve. Do not quietly fix the
|
|
70
|
+
code either. Record it as a finding: what the code does, why it looks unintended, and
|
|
71
|
+
where. Findings are announced in phase 5 and ruled on by the human at the spec summary,
|
|
72
|
+
not by you.
|
|
73
|
+
|
|
74
|
+
## 3. Draft the markdown contract
|
|
75
|
+
|
|
76
|
+
Follow the convention exactly — the drafting pitfalls `spec-feature` restates
|
|
77
|
+
(one clause per statement, ids are names not order, the body is free) all apply
|
|
78
|
+
unchanged. `SPEC.md`, `CONTEXT.md`, and `AGENTS.md` are the floor, even for a small
|
|
79
|
+
carve; `decisions/` appears with the first cross-criterion choice the code turns out
|
|
80
|
+
to embody.
|
|
81
|
+
|
|
82
|
+
The carve-specific part is `CONTEXT.md`: adopt the language the code already uses.
|
|
83
|
+
The terms are the names in the source, and the _Avoid_ lines retire the synonyms the
|
|
84
|
+
codebase mixes — a carve is often the first time anyone writes down which of three
|
|
85
|
+
interchangeable names is canonical.
|
|
86
|
+
It is a glossary only: a choice the code embodies that spans criteria (a keying
|
|
87
|
+
strategy, a rounding policy) is an ADR in `decisions/`, recording what the code does
|
|
88
|
+
and whatever "why" survives.
|
|
89
|
+
In `AGENTS.md`, state how to work the slice — run its tests, find the contract — and
|
|
90
|
+
nothing about behaviour.
|
|
91
|
+
|
|
92
|
+
## 4. Lint until clean
|
|
93
|
+
|
|
94
|
+
Resolve the oracle once, in this order, and reuse what works:
|
|
95
|
+
|
|
96
|
+
1. `<repo-root>/node_modules/.bin/speccle` — the repo's own pinned copy, put
|
|
97
|
+
there by `strength init`. A devDependency is never on `PATH` in this shell, so test
|
|
98
|
+
for the file; in a monorepo check the package you are working in as well as the
|
|
99
|
+
root. It wins over a global install: the pin is a committed choice, and lint rules
|
|
100
|
+
change between versions.
|
|
101
|
+
2. `speccle` on `PATH` — a global install.
|
|
102
|
+
3. Otherwise, from a clone of the speccle repo, run it from source — Node ≥ 24 executes
|
|
103
|
+
TypeScript directly, so no build is needed:
|
|
104
|
+
`node <speccle-repo>/packages/oracle/src/cli.ts`.
|
|
105
|
+
|
|
106
|
+
If none resolves, point the user at the install steps in Speccle's README and stop.
|
|
107
|
+
Do not hand-check the convention in the oracle's place.
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
<oracle> lint <feature-folder>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Fix and re-run until clean — exit `0` clean, `1` violations, `2` usage error; parse
|
|
114
|
+
`--json` rather than scraping the human output. When a quality violation fires on a
|
|
115
|
+
derived statement, rewrite the statement without changing what it observes: the fix for
|
|
116
|
+
"the parser handles bad input" is naming what the parser observably does with bad
|
|
117
|
+
input, which usually means going back to the code to look.
|
|
118
|
+
|
|
119
|
+
## 5. Announce the derivation — and keep going
|
|
120
|
+
|
|
121
|
+
Show two things, then proceed straight into phase 6 without waiting:
|
|
122
|
+
|
|
123
|
+
- **The criteria** — ids and statements, as `implement-feature` does. What the human
|
|
124
|
+
will rule on later is more than wording: whether your reading of the code matches
|
|
125
|
+
their intent.
|
|
126
|
+
- **The findings** — each behaviour you suspect is unintended. Spec what the code
|
|
127
|
+
observably does, suspicious or not — a carved spec is honest before it is
|
|
128
|
+
aspirational — and flag each finding here and again in the spec summary, where the
|
|
129
|
+
human rules: **intended**, and its criterion stands, stating what the code does; or
|
|
130
|
+
**a bug**, and the criterion comes out of the spec and the behaviour is recorded as
|
|
131
|
+
future work in whatever tracker the project uses. A carve never fixes one.
|
|
132
|
+
|
|
133
|
+
If the human interjects — now or at any point — treat "looks good, and also…" as a
|
|
134
|
+
change request: amend, re-lint, announce again.
|
|
135
|
+
|
|
136
|
+
## 6. Claim every criterion, changing nothing
|
|
137
|
+
|
|
138
|
+
The carve's whole discipline in one check: when this phase ends, the diff shows test
|
|
139
|
+
files and the markdown contract — `SPEC.md`, `CONTEXT.md`, `AGENTS.md`, `decisions/` —
|
|
140
|
+
**nothing else**.
|
|
141
|
+
|
|
142
|
+
- **Tag the existing tests.** A test claims a criterion when the `[KEY-n]` token
|
|
143
|
+
appears in its full concatenated name — renaming the enclosing `describe` is the
|
|
144
|
+
idiom.
|
|
145
|
+
Tag only tests that assert the criterion's behaviour, not every test that happens to
|
|
146
|
+
execute the code. Tests that map to no criterion stay as they are — untagged is
|
|
147
|
+
honest; do not delete or reword them.
|
|
148
|
+
- **Move outside tests into `src/`, beside the code they defend.** Colocation is the
|
|
149
|
+
convention's point. Fix the moved files' own imports and confirm the runner still
|
|
150
|
+
finds them — an include pattern that no longer matches loses tests silently.
|
|
151
|
+
- **Write tests for unclaimed criteria**, tagged the same way. They run against code
|
|
152
|
+
that already works, so they pass on first run. **A new test that fails is a
|
|
153
|
+
discovery, not a draft to iterate on**: either you misread the code — fix the
|
|
154
|
+
statement, re-lint, and announce the change — or you found a bug — it comes out of
|
|
155
|
+
the spec and into the tracker, like any phase-5 finding. The failing test is never a
|
|
156
|
+
reason to touch the source.
|
|
157
|
+
|
|
158
|
+
There is no tracer criterion — the tracer proves a path connects, and a carve's path is
|
|
159
|
+
proven by the code already running.
|
|
160
|
+
Claim criteria in document order, suite green at every criterion boundary.
|
|
161
|
+
|
|
162
|
+
## 7. Confirm done
|
|
163
|
+
|
|
164
|
+
Done means all five, verified rather than assumed:
|
|
165
|
+
|
|
166
|
+
1. The feature folder is named for the feature and holds the markdown contract —
|
|
167
|
+
`SPEC.md`, `CONTEXT.md`, `AGENTS.md` — with all code and tests in `src/`.
|
|
168
|
+
2. `<oracle> lint <feature-folder>` exits `0`.
|
|
169
|
+
3. Every criterion id appears in at least one full test name — run the suite with a
|
|
170
|
+
JSON reporter and check each id against the concatenated names.
|
|
171
|
+
4. The **whole project's** suite is green, not just the carved folder's — moved test
|
|
172
|
+
files are exactly the change that breaks a sibling.
|
|
173
|
+
5. The diff touches test files and the markdown contract only. Check `git status`
|
|
174
|
+
and `git diff` rather than asserting it — this is the invariant the carve exists to
|
|
175
|
+
keep, and it is the first thing to say when handing back.
|
|
176
|
+
|
|
177
|
+
Hand back with the **spec summary**: the derived criteria (ids and statements) and
|
|
178
|
+
every finding awaiting a ruling. The human rules each finding intended-or-bug here —
|
|
179
|
+
a criterion ruled a bug is reverted with its tests, and the behaviour goes to the
|
|
180
|
+
tracker.
|
|
181
|
+
|
|
182
|
+
Then say what done does not include: how well the new claims _defend_ the code is
|
|
183
|
+
`strengthen`'s question, and a fresh carve is its natural next target.
|
|
@@ -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,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conform
|
|
3
|
+
description: Bring already-governed feature folders up to the current convention after it changes — sweep every SPEC.md, diagnose each slice's drift against the convention (folder shape, an over-full flat src/, missing contract files, decisions content in CONTEXT.md, lint violations), announce the drift report, then apply form-only fixes with the suite green throughout, ending with a spec summary. Use when the user upgrades Speccle or its convention and wants existing features updated, asks to bring features up to convention or check for drift, or says "conform this", "conform the features", "the convention changed".
|
|
4
|
+
allowed-tools: Read(/${CLAUDE_PLUGIN_ROOT}/skills/*/references/**)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# conform
|
|
8
|
+
|
|
9
|
+
Bring slices that are **already governed** up to the convention as it stands today.
|
|
10
|
+
The convention moves — a folder shape tightens, a new contract file becomes the floor,
|
|
11
|
+
a lint rule sharpens — and a feature scaffolded correctly last month drifts without a
|
|
12
|
+
line of it changing.
|
|
13
|
+
Conform is the remedy: form changes only, behaviour never.
|
|
14
|
+
|
|
15
|
+
Two neighbours this skill is not. Ungoverned code — no `SPEC.md` — is a **carve**
|
|
16
|
+
(`carve-feature`); changing what a criterion _means_ is an **amend** (`spec-feature`).
|
|
17
|
+
A request that mixes conforming with either is two passes, never one.
|
|
18
|
+
|
|
19
|
+
The shape being conformed _to_ is fixed by the convention, bundled beside this skill.
|
|
20
|
+
Read `${CLAUDE_SKILL_DIR}/references/convention.md` before diagnosing anything — the
|
|
21
|
+
checklist below is applied against it, not from memory.
|
|
22
|
+
|
|
23
|
+
Speccle's words are fixed and mandatory: "conform", not "migrate" or "upgrade";
|
|
24
|
+
"criterion id", not "tag"; "lint violation", not "error"; "spec summary", not
|
|
25
|
+
"approval gate".
|
|
26
|
+
|
|
27
|
+
## 1. Find the governed slices
|
|
28
|
+
|
|
29
|
+
Given a project root, find every `SPEC.md` under it by the convention's discovery
|
|
30
|
+
rules — never entering `node_modules`, `dist`, `fixtures`, `__fixtures__`, or
|
|
31
|
+
dot-directories — and conform each. Given a single feature folder, conform just it.
|
|
32
|
+
|
|
33
|
+
No `SPEC.md` found means there is nothing governed to conform: the work is a carve or
|
|
34
|
+
a new feature. Say so and stop.
|
|
35
|
+
|
|
36
|
+
## 2. Diagnose each slice against the bundled convention
|
|
37
|
+
|
|
38
|
+
Resolve the oracle once, in this order, and reuse what works:
|
|
39
|
+
|
|
40
|
+
1. `<repo-root>/node_modules/.bin/speccle` — the repo's own pinned copy, put
|
|
41
|
+
there by `strength init`. A devDependency is never on `PATH` in this shell, so test
|
|
42
|
+
for the file; in a monorepo check the package you are working in as well as the
|
|
43
|
+
root. It wins over a global install: the pin is a committed choice, and lint rules
|
|
44
|
+
change between versions.
|
|
45
|
+
2. `speccle` on `PATH` — a global install.
|
|
46
|
+
3. Otherwise, from a clone of the speccle repo, run it from source — Node ≥ 24 executes
|
|
47
|
+
TypeScript directly, so no build is needed:
|
|
48
|
+
`node <speccle-repo>/packages/oracle/src/cli.ts`.
|
|
49
|
+
|
|
50
|
+
If none resolves, point the user at the install steps in Speccle's README and stop.
|
|
51
|
+
Do not hand-check the spec rules in the oracle's place.
|
|
52
|
+
|
|
53
|
+
Then walk the checklist for each slice — every item is what the convention requires
|
|
54
|
+
today, and the bundled copy is the authority:
|
|
55
|
+
|
|
56
|
+
- **The folder is named for the feature.** An unnamed catch-all (`src/`, `lib/`)
|
|
57
|
+
flags, even for a project's only feature.
|
|
58
|
+
- **The markdown contract is complete at the root**: `SPEC.md`, `CONTEXT.md`,
|
|
59
|
+
`AGENTS.md`; `decisions/` whenever the feature has a decision to hold.
|
|
60
|
+
- **All code and tests sit under `src/`**, tests beside the code they defend, and the
|
|
61
|
+
root holds the markdown contract and nothing else. `src/` may nest into subfolders —
|
|
62
|
+
legal nesting is not drift. What flags is an **over-full flat directory**: `src/`
|
|
63
|
+
itself, or any subfolder under it, holding **more than ten files directly**. Count
|
|
64
|
+
the entries; subfolders don't count toward their parent's total.
|
|
65
|
+
- **`CONTEXT.md` is a glossary only.** Decisions content — a Decisions section,
|
|
66
|
+
mini-ADR bullets, any recorded choice spanning criteria — belongs in `decisions/`
|
|
67
|
+
as numbered ADR files.
|
|
68
|
+
- **`<oracle> lint <feature-folder>` exits 0.** Parse `--json` rather than scraping
|
|
69
|
+
the human output. A rule that did not exist when the spec was written flags here
|
|
70
|
+
like any other lint violation.
|
|
71
|
+
- **Every criterion id appears in at least one full test name.** An unclaimed
|
|
72
|
+
criterion is a **finding**, not a fix — writing tests is `carve-feature` and
|
|
73
|
+
`strengthen` territory, and conform names the gap rather than filling it.
|
|
74
|
+
|
|
75
|
+
## 3. Announce the drift report — and keep going
|
|
76
|
+
|
|
77
|
+
Per slice: each drift item found and the fix planned for it; slices with no drift
|
|
78
|
+
listed as clean. Then proceed straight into phase 4 without waiting — a misjudged fix
|
|
79
|
+
is corrected by interrupt, like any other announcement.
|
|
80
|
+
|
|
81
|
+
## 4. Apply the fixes, suite green at every boundary
|
|
82
|
+
|
|
83
|
+
Run the project's suite before touching anything. Conform never starts on red without
|
|
84
|
+
saying so — a pre-existing failure is the user's to rule on, not yours to absorb into
|
|
85
|
+
the diff.
|
|
86
|
+
|
|
87
|
+
Per slice, in this order:
|
|
88
|
+
|
|
89
|
+
- **Structural moves first.** Code and tests into `src/`; a catch-all folder renamed
|
|
90
|
+
for its feature. Fix the moved files' own imports, everything that imports them,
|
|
91
|
+
and the runner and tsconfig include patterns — a pattern that no longer matches
|
|
92
|
+
loses tests silently, so re-run the suite and check the test count, not just the
|
|
93
|
+
colour.
|
|
94
|
+
- **Regroup an over-full directory.** A `src/` or subfolder over the ten-file limit
|
|
95
|
+
gets its files gathered by concern into shallow, purpose-named subfolders — a pure
|
|
96
|
+
relocation, imports and config fixed, test count held, exactly like the moves above.
|
|
97
|
+
**Never split the slice into siblings**: a new feature folder redraws boundaries and
|
|
98
|
+
is a re-slice, not a form fix. When the pile reads as two behaviours rather than one
|
|
99
|
+
crowded feature, regroup it anyway — the subfolders you draw are the natural split
|
|
100
|
+
lines — and carry "consider splitting into siblings via a `feature` amend" into the
|
|
101
|
+
spec summary as a finding. That better fix is the human's to make, not conform's.
|
|
102
|
+
- **Generate a missing `AGENTS.md`** from what the folder shows: what the slice does
|
|
103
|
+
in a sentence or two, how to run its tests, where the contract lives. Facts about
|
|
104
|
+
working the slice, never about its behaviour.
|
|
105
|
+
- **Split decisions content out of `CONTEXT.md`** into `decisions/0001-<slug>.md`
|
|
106
|
+
onward, same form as any ADR. The original wording becomes the Decision; a "why"
|
|
107
|
+
that was never recorded is stated as unrecorded, not invented.
|
|
108
|
+
- **Reword statements only as far as lint requires, meaning unchanged.** The fix for
|
|
109
|
+
a quality violation on an old statement is naming what the code observably does —
|
|
110
|
+
which usually means reading the code, not polishing the sentence. Ids are names:
|
|
111
|
+
never renumbered, never reused. Re-run lint until the slice is clean.
|
|
112
|
+
|
|
113
|
+
## 5. Confirm done
|
|
114
|
+
|
|
115
|
+
Done means all four, verified rather than assumed:
|
|
116
|
+
|
|
117
|
+
1. `<oracle> lint` exits `0` for every slice swept.
|
|
118
|
+
2. The **whole project's** suite is green with the same test count as before the
|
|
119
|
+
moves — moved test files are exactly the change that loses a sibling silently.
|
|
120
|
+
3. The diff shows markdown, file moves with their import and config fixes, and
|
|
121
|
+
criterion-id tags in test names — nothing source-semantic. Check `git status` and
|
|
122
|
+
`git diff` rather than asserting it; this is the invariant conform exists to keep.
|
|
123
|
+
4. Every drift item announced in phase 3 is either fixed or carried into the spec
|
|
124
|
+
summary as a finding.
|
|
125
|
+
|
|
126
|
+
Hand back with the **spec summary**: per-slice changes; every reworded statement, old
|
|
127
|
+
and new side by side under its unchanged id, for the human to overrule; and the
|
|
128
|
+
findings — unclaimed criteria and anything suspicious met along the way — naming
|
|
129
|
+
`strengthen` as the natural next step where the claims look thin.
|