@ericrisco/rsc 1.0.11 → 1.0.13
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/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/fastapi/scripts/verify.sh +47 -6
- package/skills/go/scripts/verify.sh +26 -1
- package/skills/implement/SKILL.md +18 -0
- package/skills/review/SKILL.md +35 -3
- package/skills/specify/SKILL.md +31 -0
- package/skills/specify/evals/cases.yaml +2 -0
- package/skills/testing-go/SKILL.md +17 -0
- package/skills/testing-go/evals/cases.yaml +2 -0
- package/skills/testing-py/SKILL.md +18 -0
- package/skills/testing-py/evals/cases.yaml +2 -0
- package/skills/testing-web/SKILL.md +20 -0
- package/skills/testing-web/evals/cases.yaml +2 -0
- package/skills/verify/SKILL.md +47 -0
- package/skills/verify/evals/cases.yaml +5 -0
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -9,9 +9,20 @@
|
|
|
9
9
|
# Auto-detects each tool. If a tool is missing it prints a yellow SKIP and continues
|
|
10
10
|
# (it never FAILs on a missing tool). Prefers `uv run <tool>` when `uv` is present,
|
|
11
11
|
# else the bare tool on PATH. Exits non-zero only if a tool actually ran and reported
|
|
12
|
-
# a failure.
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# a failure. Idempotent: re-running yields the same result (read-only beyond whatever
|
|
13
|
+
# the project's pytest does).
|
|
14
|
+
#
|
|
15
|
+
# COVERAGE IS ONLY A GATE IF YOUR PROJECT MAKES IT ONE. The threshold is deliberately
|
|
16
|
+
# delegated to the project (--cov-fail-under in pyproject.toml / setup.cfg / .coveragerc)
|
|
17
|
+
# rather than hardcoded here. But when the project sets none, `pytest --cov` prints a
|
|
18
|
+
# percentage and exits 0 however far coverage falls — and a layer that prints a number and
|
|
19
|
+
# exits 0 is a report, not a gate. That case is reported as [gap] instead of PASS, because
|
|
20
|
+
# the two are indistinguishable on screen and the broken one can only ever produce green:
|
|
21
|
+
# no failing run will ever surface it.
|
|
22
|
+
#
|
|
23
|
+
# A [gap] does NOT change the exit code — breaking every repo that passes today is not this
|
|
24
|
+
# script's call. This script reports; the `verify` skill judges, and there a gap counts as
|
|
25
|
+
# an unverified criterion, which fails the verdict.
|
|
15
26
|
#
|
|
16
27
|
# Compatible with stock macOS bash 3.2: no `mapfile`, no associative arrays, and every
|
|
17
28
|
# array access is guarded so `set -u` never trips on an "unbound" empty array.
|
|
@@ -30,7 +41,28 @@ warn() { printf '%s%s%s\n' "$YELLOW" "$*" "$RESET"; }
|
|
|
30
41
|
ok() { printf '%s%s%s\n' "$GREEN" "$*" "$RESET"; }
|
|
31
42
|
fail() { printf '%s%s%s\n' "$RED" "$*" "$RESET"; }
|
|
32
43
|
|
|
33
|
-
PASSED=0; SKIPPED=0; FAILED=0
|
|
44
|
+
PASSED=0; SKIPPED=0; FAILED=0; GAPS=0
|
|
45
|
+
|
|
46
|
+
# gap <reason> — the layer ran and had nothing to fail with. Counted and printed, but it
|
|
47
|
+
# deliberately does NOT touch FAILED: see the header note on why the exit code is unchanged.
|
|
48
|
+
gap() { printf '%sGAP: %s%s\n' "$YELLOW" "$*" "$RESET"; GAPS=$((GAPS + 1)); }
|
|
49
|
+
|
|
50
|
+
# coverage_threshold_configured — does the project actually gate on coverage?
|
|
51
|
+
# Looks for pytest's --cov-fail-under (addopts or a bare flag) and coverage.py's fail_under.
|
|
52
|
+
# Grep rc: 0 = found, 1 = not found, >=2 = the scan itself broke. Treat "broke" as NOT
|
|
53
|
+
# configured so the answer errs toward reporting a gap; a broken scan must never buy a pass.
|
|
54
|
+
coverage_threshold_configured() {
|
|
55
|
+
local f rc
|
|
56
|
+
for f in "$TARGET/pyproject.toml" "$TARGET/setup.cfg" "$TARGET/tox.ini" "$TARGET/.coveragerc" "$TARGET/pytest.ini"; do
|
|
57
|
+
[ -f "$f" ] || continue
|
|
58
|
+
grep -qE 'cov-fail-under|fail_under' "$f" && return 0
|
|
59
|
+
rc=$?
|
|
60
|
+
if [ "$rc" -ge 2 ]; then
|
|
61
|
+
warn "NOTE: could not read '${f}' while checking for a coverage threshold; treating it as absent"
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
return 1
|
|
65
|
+
}
|
|
34
66
|
|
|
35
67
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
36
68
|
|
|
@@ -105,12 +137,21 @@ run_step "ruff check" ruff check "$TARGET"
|
|
|
105
137
|
run_step "ruff format check" ruff format --check "$TARGET"
|
|
106
138
|
run_step "mypy" mypy "$TARGET"
|
|
107
139
|
run_step "pytest + coverage" pytest --cov --cov-report=term-missing
|
|
140
|
+
# Only meaningful if pytest actually ran; a SKIP already says nothing was measured.
|
|
141
|
+
if tool_available pytest && ! coverage_threshold_configured; then
|
|
142
|
+
gap "coverage ran without a threshold (no --cov-fail-under / fail_under found) — this layer cannot fail. Add --cov-fail-under=<n> to pyproject.toml addopts to make it a gate."
|
|
143
|
+
fi
|
|
108
144
|
run_step "pip-audit" pip-audit
|
|
109
145
|
|
|
110
|
-
printf '\n%d passed, %d skipped, %d failed\n' "$PASSED" "$SKIPPED" "$FAILED"
|
|
146
|
+
printf '\n%d passed, %d skipped, %d failed, %d gap(s)\n' "$PASSED" "$SKIPPED" "$FAILED" "$GAPS"
|
|
147
|
+
if [ "$GAPS" -gt 0 ]; then
|
|
148
|
+
warn "A GAP is neither a pass nor a failure: the layer ran and could not have failed. Report it as unverified."
|
|
149
|
+
fi
|
|
111
150
|
if [ "$FAILED" -gt 0 ]; then
|
|
112
151
|
fail "verify.sh: failures detected"
|
|
113
152
|
exit 1
|
|
114
153
|
fi
|
|
115
|
-
|
|
154
|
+
# "every check that could fail, passed" rather than a bare ok: with a GAP present, the
|
|
155
|
+
# stronger claim would be false in exactly the way this script now exists to prevent.
|
|
156
|
+
ok "verify.sh: ok (every check that could fail, passed)"
|
|
116
157
|
exit 0
|
|
@@ -11,6 +11,17 @@
|
|
|
11
11
|
# Real problems (unformatted code, vet/test/vuln failures) exit non-zero.
|
|
12
12
|
# Read-only: never mutates your source.
|
|
13
13
|
#
|
|
14
|
+
# ONE LAYER HERE IS A REPORT, NOT A GATE. `go test -cover` takes no threshold flag, so
|
|
15
|
+
# coverage can print any number — 4%, 0% — and still exit 0. It is therefore reported as
|
|
16
|
+
# [gap]: the layer ran and had nothing to fail with. Said out loud because a layer that
|
|
17
|
+
# prints a number and exits 0 looks exactly like a layer that passed, and that shape can
|
|
18
|
+
# only ever produce green — no failing run will ever tell you about it. Want a real
|
|
19
|
+
# coverage gate in Go? Parse `go tool cover -func` and compare, or use a CI action; this
|
|
20
|
+
# script does not pretend to be one.
|
|
21
|
+
#
|
|
22
|
+
# A [gap] does NOT change the exit code: this script reports, the `verify` skill judges,
|
|
23
|
+
# and there it counts as an unverified criterion, which fails the verdict.
|
|
24
|
+
#
|
|
14
25
|
# Portability: runs on stock macOS bash 3.2 (no mapfile, no associative arrays,
|
|
15
26
|
# no unguarded array expansions under `set -u`).
|
|
16
27
|
|
|
@@ -24,12 +35,16 @@ else
|
|
|
24
35
|
fi
|
|
25
36
|
|
|
26
37
|
failed=0
|
|
38
|
+
gaps=0
|
|
27
39
|
|
|
28
40
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
29
41
|
warn() { printf '%s[skip]%s %s\n' "$YELLOW" "$RESET" "$*"; }
|
|
30
42
|
fail() { printf '%s[fail]%s %s\n' "$RED" "$RESET" "$*"; failed=1; }
|
|
31
43
|
ok() { printf '%s[ ok ]%s %s\n' "$GREEN" "$RESET" "$*"; }
|
|
32
44
|
info() { printf -- '----- %s\n' "$*"; }
|
|
45
|
+
# A layer that ran but had nothing to fail with. Deliberately does NOT set `failed`:
|
|
46
|
+
# breaking every repo that passes today is not this script's call to make.
|
|
47
|
+
gap() { printf '%s[gap ]%s %s\n' "$YELLOW" "$RESET" "$*"; gaps=$((gaps + 1)); }
|
|
33
48
|
|
|
34
49
|
# Must run from a module root.
|
|
35
50
|
if [ ! -f go.mod ]; then
|
|
@@ -89,6 +104,10 @@ if have go; then
|
|
|
89
104
|
warn "cgo disabled or C compiler '$cgo_cc' not found; running plain go test -cover"
|
|
90
105
|
if go test -cover ./...; then ok "tests pass (no race)"; else fail "tests failed"; fi
|
|
91
106
|
fi
|
|
107
|
+
# The tests above are a gate; the coverage number printed alongside them is not.
|
|
108
|
+
# `go test -cover` has no threshold flag, so coverage cannot fail this script no matter
|
|
109
|
+
# how far it falls. Reported so nobody reads the percentage as a checked constraint.
|
|
110
|
+
gap "coverage: reported, not gated — 'go test -cover' takes no threshold, so this layer cannot fail"
|
|
92
111
|
else
|
|
93
112
|
warn "go not found - skipping tests"
|
|
94
113
|
fi
|
|
@@ -102,8 +121,14 @@ else
|
|
|
102
121
|
fi
|
|
103
122
|
|
|
104
123
|
echo
|
|
124
|
+
if [ "$gaps" -ne 0 ]; then
|
|
125
|
+
printf '%s%d layer(s) ran without being able to fail (see [gap] above).%s\n' "$YELLOW" "$gaps" "$RESET"
|
|
126
|
+
printf 'Not a pass and not a failure: report them as unverified rather than green.\n'
|
|
127
|
+
fi
|
|
105
128
|
if [ "$failed" -ne 0 ]; then
|
|
106
129
|
printf '%sFAIL:%s one or more checks failed.\n' "$RED" "$RESET"
|
|
107
130
|
exit 1
|
|
108
131
|
fi
|
|
109
|
-
|
|
132
|
+
# Deliberately "checks that could fail, passed" and not "all checks passed": with a [gap]
|
|
133
|
+
# present the second sentence would be false in the way this whole change exists to stop.
|
|
134
|
+
printf '%sPASS:%s every check that could fail, passed.\n' "$GREEN" "$RESET"
|
|
@@ -76,6 +76,24 @@ code tells you nothing about whether it can fail. **Refactor only on green** kee
|
|
|
76
76
|
reversible against a passing bar. **Checkpoint after each task** is what makes this resumable and
|
|
77
77
|
reviewable instead of a 2000-line surprise diff.
|
|
78
78
|
|
|
79
|
+
### Four things you may never do to reach green
|
|
80
|
+
|
|
81
|
+
The loop only means anything if green cannot be manufactured. These are absolute, and each one is a
|
|
82
|
+
way people reach green without fixing anything:
|
|
83
|
+
|
|
84
|
+
1. **Never weaken a test to make it pass.** Not broadening an assertion, not adding a skip, not
|
|
85
|
+
raising a tolerance, not deleting it. A test that looks wrong is a **spec conversation** — surface
|
|
86
|
+
it, don't bury it.
|
|
87
|
+
2. **Never edit a test and the implementation in the same step to reach green.** Change one, run,
|
|
88
|
+
then the other. Simultaneous edits let you redefine correctness to match your bug without
|
|
89
|
+
noticing you did it.
|
|
90
|
+
3. **Never mock the unit under test**, or mock so much that the test only exercises the mocks. Mock
|
|
91
|
+
boundaries you don't own — network, clock, filesystem, third-party SDK — never the logic you are
|
|
92
|
+
being paid to verify.
|
|
93
|
+
4. **Never chase the coverage number.** Coverage detects untested code; it is not a target. A test
|
|
94
|
+
added to touch lines, with no meaningful assertion, is gaming — and mutation testing exists
|
|
95
|
+
precisely to catch it (`../testing-py/SKILL.md`, `../testing-web/SKILL.md`, `../testing-go/SKILL.md`).
|
|
96
|
+
|
|
79
97
|
### The done-check is the contract
|
|
80
98
|
|
|
81
99
|
Each task carries a done-check from the `tasks` phase ("returns 409 on duplicate email", "list
|
package/skills/review/SKILL.md
CHANGED
|
@@ -128,9 +128,41 @@ gap deliberately with `--accept-partial-lenses` (it is recorded). Every approval
|
|
|
128
128
|
|
|
129
129
|
**Lenses by risk tier** — tier 0 never reaches you (the gate passes docs/copy silently);
|
|
130
130
|
tier 1 → run the single most relevant pass from the table above yourself; tier 2 → dispatch
|
|
131
|
-
**three parallel fresh-context subagents** (correctness · security · tests-as-evidence), each
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
**three parallel fresh-context subagents** (correctness · security · tests-as-evidence), each told
|
|
132
|
+
to *refute* readiness, not confirm it. Fresh context is the point: a reviewer who inherits the
|
|
133
|
+
implementer's context inherits its blind spots.
|
|
134
|
+
|
|
135
|
+
**Give each refuter exactly four inputs — no more, no less.** The diff alone is not enough, and
|
|
136
|
+
that is not a detail: with only the diff, the one failure class no test suite can structurally
|
|
137
|
+
catch — *the change is correct and is not what the approved spec asked for* — is invisible to the
|
|
138
|
+
whole panel.
|
|
139
|
+
|
|
140
|
+
1. **The task contract** — the original request **plus every scope change a human explicitly
|
|
141
|
+
approved since.** Not just the first message: without the approved changes a legitimate scope
|
|
142
|
+
revision reads as a spec gap and you get a confident false positive.
|
|
143
|
+
2. **The approved spec** (`02-DOCS/wiki/sdd/specs/<slug>.md`).
|
|
144
|
+
3. **The exact source state** — commit SHA, or a tree hash when git is absent. A verdict belongs to
|
|
145
|
+
the state that was reviewed, not to the project.
|
|
146
|
+
4. **The entry point** — the one command that reruns the checks.
|
|
147
|
+
|
|
148
|
+
**And say what they do not get:** your conversation, your reasoning, your defences, and your draft
|
|
149
|
+
verdict. If a claim needs your justification to stand, it is not proven.
|
|
150
|
+
|
|
151
|
+
**Blind first, compare second.** Each refuter records what it attacked and what it found *before*
|
|
152
|
+
being shown your conclusions. Only then may it compare and add findings — the blind record is
|
|
153
|
+
append-only after that, never rewritten. Skip this and the fresh context is spent confirming your
|
|
154
|
+
framing, which is the one thing it was bought to avoid.
|
|
155
|
+
|
|
156
|
+
**The attack list is the deliverable, not just the findings.** "Nothing found" without saying where
|
|
157
|
+
you looked is indistinguishable from not having looked — the same rule that governs a dismissal.
|
|
158
|
+
|
|
159
|
+
Two extra finding classes the four inputs unlock, neither reachable from a diff:
|
|
160
|
+
|
|
161
|
+
- **Contract vs spec** — what would a caller reasonably expect, given the stated deployment, that
|
|
162
|
+
no clause covers? An exclusion the spec deliberately approved is **not** a finding; an exclusion
|
|
163
|
+
the spec describes *inaccurately* is.
|
|
164
|
+
- **Mapping, both directions** — an acceptance criterion with no test that can be made to fail, and
|
|
165
|
+
a test pinning behaviour no criterion asked for.
|
|
134
166
|
|
|
135
167
|
**A finding blocks only if it survives all three filters** — no exceptions, and eagerness to
|
|
136
168
|
find something is not evidence:
|
package/skills/specify/SKILL.md
CHANGED
|
@@ -130,6 +130,35 @@ irreversible, or large scope), dispatch a **fresh-context subagent** to read the
|
|
|
130
130
|
**Skip it** for L0/L1 on a small, low-risk spec — the self-review scan is enough there; don't spin up
|
|
131
131
|
a subagent to vet a two-paragraph spec. Like the rest of the chain, ceremony scales to the stakes.
|
|
132
132
|
|
|
133
|
+
### Approval is its own exchange (step 9)
|
|
134
|
+
|
|
135
|
+
**An answer to a question is not an approval.** If you asked the user to decide something, they
|
|
136
|
+
answered *that question* and nothing else. Their answer is an **input** to the spec — and it
|
|
137
|
+
*changes* the spec, which means any approval you were holding before the question is the approval
|
|
138
|
+
of a document that no longer exists.
|
|
139
|
+
|
|
140
|
+
Questions and approval are two exchanges, in that order:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
ask → user answers → fold it in → say what changed → show the revised spec → ask for approval
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **The recommended-option shape is where this fails most easily.** When the user picks the option
|
|
147
|
+
you recommended, the spec *looks* unchanged and consent *looks* implied. Neither is true — you
|
|
148
|
+
proposed, they chose, the spec moved.
|
|
149
|
+
- **None of these are approval:** an answer to your question; a "go ahead" about some other step;
|
|
150
|
+
silence; the request that started the task. If you cannot quote the words that approved **this**
|
|
151
|
+
spec, you do not have approval.
|
|
152
|
+
- **Autopilot is still valid** — an up-front "run it all yourself" authorizes the whole run, and
|
|
153
|
+
that is a real answer to a real question. What changes is the bookkeeping: record it as
|
|
154
|
+
**approved in autopilot, not item by item**, in the spec's `status:`. The spec then becomes the
|
|
155
|
+
artifact the user reviews *after the fact*, and the work claims correspondingly less certainty
|
|
156
|
+
about having captured their intent. Writing "aprobada" flat, when what happened was a blanket
|
|
157
|
+
go-ahead, is the record telling a small lie about the strongest gate in the chain.
|
|
158
|
+
- **Rejection is information — keep it.** If the user turns the spec down, revise it in place and
|
|
159
|
+
append the reason to `## Revisions`. Never delete and start clean: what they rejected, and why,
|
|
160
|
+
is the most useful thing in the file.
|
|
161
|
+
|
|
133
162
|
## Worked shape (abridged)
|
|
134
163
|
|
|
135
164
|
```markdown
|
|
@@ -206,6 +235,8 @@ The proposal is allowed to mention options and tradeoffs; the spec that follows
|
|
|
206
235
|
| Skip the whole loop because "this is too simple to design" | That's the rationalization that wastes the most work. Every feature gets the loop; only a literal one-line, zero-risk change skips. |
|
|
207
236
|
| Present one approach as the answer | Offer 2-3 distinct directions with trade-offs and a recommendation; let the user choose before you write the spec. |
|
|
208
237
|
| Hand to `plan`/`implement` before the user approved the written spec | The approval at step 9 is the gate. No design approved → nothing gets built. |
|
|
238
|
+
| Treat the answer to your question as the approval of the spec | They answered the question, and the answer *changed* the spec. Fold it in, say what changed, show it, ask again. |
|
|
239
|
+
| Write `status: aprobada` after a blanket "just run it" | That is autopilot. Record it as such — the record must not overstate the strongest gate in the chain. |
|
|
209
240
|
| Skip non-goals because "it's obvious" | Unsaid scope becomes assumed scope. State what you are *not* doing. |
|
|
210
241
|
| Resolve every ambiguity yourself to look finished | Inventing answers is worse than naming gaps. List them in Points to clarify. |
|
|
211
242
|
| Start designing the solution because it's clearer | Stay on WHAT/WHY. The plan is a later, separate phase. |
|
|
@@ -78,3 +78,5 @@ capability:
|
|
|
78
78
|
- "Captures observable behaviour (what the user sees/can do) and explicit non-goals / out-of-scope rather than implementation (no file format library, storage, or job-queue detail)."
|
|
79
79
|
- "Turns answers into binary acceptance criteria and defers unresolved items (e.g. data scope, retention, format) to Points to clarify instead of fabricating decisions."
|
|
80
80
|
- "Notes that the spec is complete-enough-to-plan with open points remaining, and hands off to clarify — not to plan or implementation."
|
|
81
|
+
- "After the user answers a question, folds the answer in, states what changed, shows the revised spec, and asks for approval AGAIN — it does not treat the answer as approval of the spec, including when the user picked the recommended option."
|
|
82
|
+
- "If told to run the whole chain up front, records the approval as autopilot rather than item-by-item, and says the spec is the artifact to review after the fact."
|
|
@@ -169,6 +169,23 @@ go build -cover -o ./bin/app . # coverage for integratio
|
|
|
169
169
|
|
|
170
170
|
A coverage **percentage is a smoke alarm, not a goal.** 100% line coverage with no assertions on the values is worthless; 70% on the branches that actually carry logic is fine. Use `-coverpkg=./...` when your tests live in a separate package and exercise code across the module, or the number undercounts. Profiles, integration-binary coverage, and reading the HTML report are in `references/coverage-and-benchmarks.md`.
|
|
171
171
|
|
|
172
|
+
## Mutation: does the suite actually notice?
|
|
173
|
+
|
|
174
|
+
Coverage tells you which code ran; it cannot tell you whether any test would have **noticed** if that code were wrong. A table-driven test whose cases all assert `err == nil` and nothing else raises coverage without detecting anything. Mutation testing plants bugs on purpose — flip a `<` to `<=`, drop an early return, replace a returned value with the zero value — and a mutant the suite still passes is a test that asserts nothing.
|
|
175
|
+
|
|
176
|
+
This is **not** fuzzing. Fuzzing hunts inputs your code mishandles; mutation hunts tests that would not catch a bug you already know about. They find different things and neither substitutes for the other.
|
|
177
|
+
|
|
178
|
+
Go has **no mature mutation tool** — `go-mutesting` and `gremlins` exist but neither is a dependable default — so this is the manual procedure, and that makes the discipline stricter here, not looser:
|
|
179
|
+
|
|
180
|
+
1. Pick the changed logic. Script the run and **commit the script** (e.g. `tools/mutants.go` or a shell driver); do not hand-edit N times, because you will need to rerun it and a restore mistake is silent.
|
|
181
|
+
2. One mutant at a time: flip a comparison, off-by-one a bound or slice index, delete one branch, swap `&&`/`||`, return the zero value. `go test ./...` must fail for every one.
|
|
182
|
+
3. Restore, confirm with `git diff` that nothing else moved, and rerun the suite green.
|
|
183
|
+
4. Report as `N/N killed`, naming the mutants.
|
|
184
|
+
|
|
185
|
+
**The runner must prove it executed each mutant.** A hand-rolled driver that can report a kill it never ran only ever *inflates* the score — so that defect can never surface as a red run, and no failing test will ever tell you about it. Assert the mutated file changed on disk (compare hashes before and after) and that the test command actually exited non-zero for the reason you expect, rather than trusting the loop.
|
|
186
|
+
|
|
187
|
+
**A survivor is not automatically a failure.** Some mutants are semantically equivalent and cannot be killed; classify those with the reason instead of writing a test that asserts non-behaviour. Scale the whole layer to risk (P7): reach for it where a bug is expensive — money, auth, data loss, concurrency, a public API — not on every change.
|
|
188
|
+
|
|
172
189
|
## Benchmarks
|
|
173
190
|
|
|
174
191
|
Use `for b.Loop()` (Go 1.24+), not the legacy `for i := 0; i < b.N; i++`. `b.Loop` manages the timer itself (setup before the loop and teardown after are excluded automatically), runs the body the right number of times, and the compiler is taught **not** to dead-code-eliminate calls inside it.
|
|
@@ -42,3 +42,5 @@ capability:
|
|
|
42
42
|
- A hand-written interface fake or net/http/httptest server for the HTTP dependency, not a mock framework by default
|
|
43
43
|
- Runs with -race for the concurrent path and shows a go test -coverprofile / cross-package coverage command
|
|
44
44
|
- A benchmark using for b.Loop() with b.ReportAllocs() rather than a b.N loop
|
|
45
|
+
- "Does not present the coverage percentage as proof the suite detects bugs: states Go has no mature mutation tool, gives the manual procedure, and distinguishes mutation (tests that would not notice a bug) from fuzzing (inputs the code mishandles)."
|
|
46
|
+
- "Requires a hand-rolled mutant runner to prove it executed each mutant, and says why — a kill reported without running can only inflate the score, so no failing run would ever reveal it."
|
|
@@ -139,6 +139,24 @@ pytest --cov=mypkg --cov-branch --cov-report=term-missing --cov-fail-under=85
|
|
|
139
139
|
|
|
140
140
|
Line coverage lies because executing a line is not the same as testing its outcomes. A function with `if x: a() else: b()` reaches 100% line coverage from a single test that only takes the `if` arm — the `else` ships untested. `--cov-branch` (or `branch = true`) forces both arms to be exercised, which is exactly where happy-path-only suites leak bugs. `term-missing` prints the unhit line and branch numbers so you know what to write next. Exclude only code that is correctly never run by tests (`if TYPE_CHECKING:`, `raise NotImplementedError`, `__repr__`) — never exclude a branch just to make the number go up.
|
|
141
141
|
|
|
142
|
+
## Mutation: does the suite actually notice?
|
|
143
|
+
|
|
144
|
+
Branch coverage tells you which code ran. It cannot tell you whether any test would have **noticed** if that code were wrong — and a test with no assertion, or an assertion that cannot fail, raises coverage without detecting anything. Mutation testing closes that gap by planting bugs on purpose: if the suite still passes, the mutant *survived* and you have found a test that asserts nothing.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# pyproject.toml → [tool.mutmut] source_paths = ["src/"]
|
|
148
|
+
mutmut run # whole source tree
|
|
149
|
+
mutmut run "mypkg.core*" # scope it — this is the normal case
|
|
150
|
+
mutmut results # what survived
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Reach for the real tool, not a hand-written mutant list: mutmut generates mutants from the syntax tree, so it cannot apply one to code that moved and cannot report one it never ran.
|
|
154
|
+
|
|
155
|
+
- **Scale it to risk.** This is not a default toll — it is minutes-to-hours on a real tree. Run it where a bug is expensive (money, auth, data loss, concurrency, a public API) or where a green suite keeps shipping bugs anyway. Always scope to the code you changed.
|
|
156
|
+
- **A survivor is not automatically a failure.** Some mutants are semantically equivalent to the original and cannot be killed. Classify those with the reason ("equivalent: both branches return the same value for every reachable input"). Never add a test that asserts non-behaviour just to kill one — that is the same gaming that coverage-chasing is.
|
|
157
|
+
- **A survivor that is a real bug gets a test, not an excuse.** Write the assertion that kills it, then rerun.
|
|
158
|
+
- **If you hand-roll a runner, prove it executed every mutant.** Clear `__pycache__` and set `PYTHONDONTWRITEBYTECODE` per mutant: two same-size mutants written in the same second can share a bytecode cache, and the runner then reports a kill for code it never ran. That defect can only ever *inflate* the score, so it will never show up as a red run — which is why the proof has to be built in rather than assumed.
|
|
159
|
+
|
|
142
160
|
## Property testing
|
|
143
161
|
|
|
144
162
|
Reach for Hypothesis when a fixed example matrix can't cover the input space: parsers, serializers, encoders, math, anything with an invariant. You assert a *property* that must hold for all inputs and let Hypothesis hunt counterexamples.
|
|
@@ -42,3 +42,5 @@ capability:
|
|
|
42
42
|
- "asserts on the return value / observable behavior, not on mock private internals like _mock_calls"
|
|
43
43
|
- "mentions a strict posture: --strict-markers and/or --strict-config (or the pytest 9 strict_* aliases)"
|
|
44
44
|
- "at least one negative/error-path test (e.g. raises on non-200), not only the happy path"
|
|
45
|
+
- "Does not present branch coverage as proof the suite detects bugs: names mutation testing (mutmut) as the layer that catches assertion-free tests, and scopes it to the changed module rather than the whole tree."
|
|
46
|
+
- "Calibrates mutation by risk rather than prescribing it as a default toll, and says a surviving mutant may be semantically equivalent — to be classified with a reason, never killed with an assertion about non-behaviour."
|
|
@@ -194,6 +194,26 @@ didn't change, and a giant DOM snapshot gets blindly `--updated` the first time
|
|
|
194
194
|
small, stable, serializable output (a formatted currency string, a normalized config object). Never
|
|
195
195
|
snapshot a full component tree as your primary assertion.
|
|
196
196
|
|
|
197
|
+
## Mutation: does the suite actually notice?
|
|
198
|
+
|
|
199
|
+
Coverage tells you which code ran. It cannot tell you whether any test would have **noticed** if that code were wrong — and a `render()` with no assertion, or a snapshot nobody reads, raises coverage while detecting nothing. Mutation testing plants bugs on purpose: if the suite still passes, the mutant *survived* and you have found a test that asserts nothing.
|
|
200
|
+
|
|
201
|
+
```jsonc
|
|
202
|
+
// stryker.config.json — scope is not optional here
|
|
203
|
+
{ "testRunner": "vitest", "mutate": ["src/cart/total.ts", "src/cart/discount.ts"] }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npx stryker run
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Reach for the real tool over a hand-written mutant list: Stryker generates mutants from the syntax tree, so it cannot apply one to code that moved and cannot report one it never ran.
|
|
211
|
+
|
|
212
|
+
- **Always scope `mutate` to the files you changed.** A whole-project Stryker run on a real front-end does not finish in a useful amount of time; that is the main reason teams try it once and abandon it.
|
|
213
|
+
- **Scale it to risk.** Not a default toll. Run it on the logic where a bug is expensive — pricing, totals, permissions, anything money- or auth-shaped — not on presentational components, where a surviving mutant usually just means the DOM detail genuinely does not matter.
|
|
214
|
+
- **A survivor is not automatically a failure.** Some mutants are semantically equivalent to the original and cannot be killed; classify those with the reason. Never add an assertion about non-behaviour just to kill one — that is coverage-chasing wearing a different hat.
|
|
215
|
+
- **A survivor that is a real bug gets an assertion, not an excuse.** Write it, then rerun.
|
|
216
|
+
|
|
197
217
|
## Anti-patterns
|
|
198
218
|
|
|
199
219
|
| Anti-pattern | Why it's wrong | Do instead |
|
|
@@ -50,3 +50,5 @@ capability:
|
|
|
50
50
|
- Tests an error path (500 -> visible error / role=alert) in a separate case
|
|
51
51
|
- No bare act() misuse in the component test; no assertions on state/props/className
|
|
52
52
|
- At least one real expect(...) assertion per test
|
|
53
|
+
- "Does not present coverage as proof the suite detects bugs: names Stryker as the layer that catches assertion-free tests, and scopes `mutate` to the changed files because a whole-project run does not finish."
|
|
54
|
+
- "Calibrates mutation by risk rather than prescribing it always, and treats a surviving mutant as possibly equivalent (classified with a reason) instead of adding an assertion about non-behaviour."
|
package/skills/verify/SKILL.md
CHANGED
|
@@ -80,10 +80,20 @@ If `config.yaml` provides `testing.commands.verify`, run those commands first or
|
|
|
80
80
|
Rules for RUN:
|
|
81
81
|
|
|
82
82
|
- The stack `verify.sh` **skips missing tools** (yellow SKIP) rather than failing on them — so a SKIP is not a pass. Note every SKIP; a skipped test suite means that criterion is **unverified**, which is not the same as verified-passing.
|
|
83
|
+
- A `GAP` is the third thing, and it is the one that looks most like a pass: **the layer ran and had nothing to fail with.** The fastapi gate reports it when coverage runs with no `--cov-fail-under` configured; the Go gate reports it always, because `go test -cover` takes no threshold. The script deliberately does not fail on a GAP — that judgement is yours. **Treat a GAP as an unverified criterion, which fails the verdict**, and record it (see the mapping below). A gate that reports and a gate that checks are indistinguishable on screen, and only the broken one is guaranteed to stay green.
|
|
83
84
|
- A non-zero exit from a tool that actually ran is a hard FAIL. Record the failing tool and its output.
|
|
84
85
|
- Run from the directory the stack skill documents (Go runs from the module root; others from the subproject root). Don't guess paths.
|
|
85
86
|
- If no `scripts/verify.sh` exists for a touched stack, that's a gap — say the gate is incomplete and recommend the user add the stack skill, rather than hand-rolling a one-off check that drifts from the real gate.
|
|
86
87
|
|
|
88
|
+
#### A home-grown gate must prove it can fail
|
|
89
|
+
|
|
90
|
+
The dangerous way a checker breaks is not a crash — it is **fail-open**: nothing errors, the layer prints pass, and it prints pass forever. That failure can only ever produce green, so no failing run will ever surface it. The rule follows: **before you trust a home-grown gate's pass, watch it fail** on a known-bad input. It is the RED principle applied to the checker.
|
|
91
|
+
|
|
92
|
+
- **Third-party tools are exempt.** pytest, mypy, tsc, eslint, ruff, `go vet` have earned their failure behavior. This rule is for what we wrote: grep gates, one-off scripts, custom guards, anything whose exit codes nobody has tested.
|
|
93
|
+
- **A must-find-nothing grep has three outcomes, not two.** No matches = pass. A match = fail. **The scan itself breaking** (unreadable input, bad pattern) = fail too. Left unhandled, an unreadable file becomes a silent pass. No `|| true`, no `2>/dev/null`, no bare fallthrough.
|
|
94
|
+
- **State the limit where you state the pass.** Watching a gate fail once proves that *one* known-bad case reaches its failure path. It does **not** prove the gate recognizes every violation of the rule it claims to enforce — a gate can fail closed perfectly and still guard a spelling rather than a behavior. Record the control, and record what it does not buy.
|
|
95
|
+
- **Our own known fail-open:** the stack `verify.sh` scripts SKIP a missing tool instead of failing. That is a documented gap, not a pass — so it goes in the record's *Capas no ejecutadas* under `HERRAMIENTA-AUSENTE`, never left implied.
|
|
96
|
+
|
|
87
97
|
### 3 — WALK the done-checks and acceptance criteria
|
|
88
98
|
|
|
89
99
|
The stack gate proves the code is *clean and tested*. It does **not** prove the feature does what the spec asked. Walk both lists explicitly:
|
|
@@ -106,6 +116,7 @@ tags: [sdd, verification]
|
|
|
106
116
|
timestamp: YYYY-MM-DDTHH:MM:SSZ
|
|
107
117
|
topic: sdd
|
|
108
118
|
slug: <slug>
|
|
119
|
+
source_state: 3f9a1c2 (clean)
|
|
109
120
|
---
|
|
110
121
|
|
|
111
122
|
# Verification — <slug> — YYYY-MM-DD
|
|
@@ -122,9 +133,42 @@ slug: <slug>
|
|
|
122
133
|
- [x] AC1 user can place an order — evidence: e2e test green
|
|
123
134
|
- [ ] AC3 duplicate submit is a no-op — UNVERIFIED: no observable proof
|
|
124
135
|
|
|
136
|
+
## Capas no ejecutadas
|
|
137
|
+
- **NO-APLICA** — mutation: no logic module changed, only templates.
|
|
138
|
+
- **HERRAMIENTA-AUSENTE** — mypy: not installed in this environment; verify.sh printed SKIP and **nothing ran in its place**.
|
|
139
|
+
- **SUSTITUIDA** — suite health: ran the suite twice in a fixed order instead of randomized. Cannot detect whole-suite order dependence.
|
|
140
|
+
|
|
141
|
+
## Hallazgos descartados
|
|
142
|
+
- "the retry loop can spin forever" — dismissed: `client.py:88` caps it at 3 attempts; test_client.py::test_retry_cap covers it.
|
|
143
|
+
|
|
125
144
|
## Verdict: FAIL — 2 open items (T4 done-check, AC3). Not ready for review.
|
|
126
145
|
```
|
|
127
146
|
|
|
147
|
+
**`source_state:` is not decoration.** A verdict belongs to the commit it was measured against, not to the project: without it, nobody can tell whether the record describes the code that shipped. Record the short SHA plus `clean` or `dirty` — a dirty tree means the record describes bytes that exist on no commit, which is worth knowing when someone reads it three weeks later.
|
|
148
|
+
|
|
149
|
+
**All numbers come from one fresh run made after the last edit.** A figure from mid-task is stale and does not go in the record, however true it was when you saw it.
|
|
150
|
+
|
|
151
|
+
**Split "didn't run" three ways**, because one `SKIP` list collapses three different confidence claims and they read identically:
|
|
152
|
+
|
|
153
|
+
| Label | Means | Reader should conclude |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `NO-APLICA` | the project has no such surface | nothing missing; this describes the project |
|
|
156
|
+
| `HERRAMIENTA-AUSENTE` | the surface exists, the tool was missing, **nothing ran** | that failure class was not looked for at all |
|
|
157
|
+
| `SUSTITUIDA` | something else ran instead | say what the substitute **cannot** detect; never write this as a pass |
|
|
158
|
+
|
|
159
|
+
`SUSTITUIDA` is the dangerous one. "Ran the suite twice" is not *suite health: stable* — it is a substitute that cannot see whole-suite order dependence, and a reader who can't tell the two apart reads "found nothing" where the truth is "did not look with that instrument".
|
|
160
|
+
|
|
161
|
+
**Where a `GAP` goes — and do not invent a fourth label.** A layer that ran without being able to fail is not `NO-APLICA` (the surface exists) and not `HERRAMIENTA-AUSENTE` (the tool ran). It is `SUSTITUIDA`: what ran was **a report in place of a gate**, and what it cannot detect is **coverage falling**. Write it that way:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
- **SUSTITUIDA** — coverage: ran without a threshold (GAP). A report, not a gate;
|
|
165
|
+
cannot detect coverage regressing. Fix: --cov-fail-under=<n> in pyproject addopts.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**And while you are there: the global percentage is the wrong number.** A project-wide floor barely moves when your change lands untested — 200 new uncovered lines in a 20k-line codebase is a rounding error against an 85% floor, so the gate passes and the change is untested. What matters is whether **the lines you touched** are exercised: `diff-cover coverage.xml --fail-under=100` gates exactly those. This is guidance, not something the generated `verify.sh` scripts do — recommend it, don't assume it ran.
|
|
169
|
+
|
|
170
|
+
**A dismissed finding carries evidence, one line each.** A fix is self-evidencing — the test now passes. A dismissal is not: "not a real problem" is indistinguishable from "did not check". Name the command, the `file:line`, or the test that disproves it, or write "ninguno".
|
|
171
|
+
|
|
128
172
|
Append-only spirit: don't overwrite a prior run's record; a new run is a new dated file. The record is the receipt the `review` and `ship` phases trust.
|
|
129
173
|
|
|
130
174
|
### 5 — VERDICT
|
|
@@ -146,6 +190,9 @@ Append-only spirit: don't overwrite a prior run's record; a new run is a new dat
|
|
|
146
190
|
| "A test is failing — let me just fix it real quick." | That's `debug`, a different discipline. `verify` reports the failure and hands off; it does not patch mid-gate. |
|
|
147
191
|
| "I'll write the verdict, then run the checks to confirm." | Backwards. RUN and WALK first; the verdict is the *consequence* of output you already read. |
|
|
148
192
|
| "The gate is mostly green, I'll call it done." | If you cannot point at the line of command output that proves a claim, you have no claim. Go run it. |
|
|
193
|
+
| "I looked at that finding; it's not a real problem." | A dismissal with no evidence is indistinguishable from a check you never made. Name the command, `file:line`, or test that disproves it — or fix it. |
|
|
194
|
+
| "The record is dated, that's enough to trace it." | A date does not identify code. Without `source_state`, nobody can tell whether the record describes what shipped. |
|
|
195
|
+
| "My grep gate passed, so the tree is clean." | Did you ever watch it fail? An unhandled broken scan exits like a pass. Prove it can fail before you trust its green. |
|
|
149
196
|
|
|
150
197
|
## Result envelope
|
|
151
198
|
|
|
@@ -72,3 +72,8 @@ capability:
|
|
|
72
72
|
- "RECORDS a dated verification file under 02-DOCS/wiki/sdd/verifications/ and indexes it in the root CLAUDE.md Knowledge map."
|
|
73
73
|
- "Returns a VERDICT that is PASS only if EVERY item has passing evidence; a single unverified acceptance criterion makes the whole verdict FAIL — and refuses to call it 'done' otherwise."
|
|
74
74
|
- "On PASS, points to the review phase; on FAIL, routes failing checks to debug and missing coverage back to implement."
|
|
75
|
+
- "Treats a coverage GAP (the layer ran with no threshold configured, so it could not have failed) as an unverified criterion that fails the verdict — not as a pass — and records it as SUSTITUIDA: a report in place of a gate, unable to detect coverage regressing."
|
|
76
|
+
- "Before trusting any home-grown gate that reports green, asks for it to be watched failing on a known-bad input, and states the limit: one negative control proves one bad case reaches the failure path, not that the gate covers the rule it claims to enforce."
|
|
77
|
+
- "Splits layers that did not run into NO-APLICA / HERRAMIENTA-AUSENTE / SUSTITUIDA rather than one undifferentiated SKIP list, and never writes SUSTITUIDA as a pass."
|
|
78
|
+
- "Binds the record to a source state (short commit SHA plus clean/dirty) and takes every number from one fresh run made after the last edit, refusing to reuse mid-task figures."
|
|
79
|
+
- "Gives any dismissed finding its own evidence line (command, file:line, or the test that disproves it) rather than asserting it was not a real problem."
|