Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .guardrails/state/recurrence.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ Stale entries fail silently (a loose rule routed to the wrong tier, or a
suppression the auditor no longer recognizes), so this review is part of any
tool upgrade — add/adjust with a test.

A **third failure mode** an upgrade can introduce is neither of the two above:
an analyzer that still runs, still exits 0, and still reports — **wrongly**.
Rule ids and suppression syntax are the id-shaped drifts; runner integrity is
the behavioural one. `guardrails-core/test/drift/stryker-runner.test.ts` holds
it: real stryker, through the vitest runner, over code whose tests kill every
mutant, asserting kills come back. It exists because vitest 5 silently inverted
the mutation gate (stryker-js#6210 — see plan.md's fourth-adoption findings) and
nothing in this repo noticed. If you add an analyzer whose value depends on a
framework integration, it needs a live guard of the same shape: a fixture whose
expected finding is known, run through the real tool.

The **id-existence half of this review is now mechanized**: the drift-guard
(`guardrails-core/test/drift/registry.test.ts`) fails the build if a hardcoded
loose id or knip issue type no longer exists upstream. You must still review the
Expand Down
36 changes: 28 additions & 8 deletions docs/guardrails/adopting-guardrails.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,23 @@ maintaining lint rules for somebody else's repository. That's this step's job:
the seeded config is knip-dirty out of the box in exactly the
knip-plus-stryker combination step 3 recommends.

**Check the runner plugin against your test framework's installed major.**
`@stryker-mutator/vitest-runner` declares `vitest: ">=2.0.0"`, so npm installs
it happily against a vitest major it cannot drive — and the failure is silent
and INVERTED: every covered mutant comes back `Survived`, so the gate
manufactures violations rather than missing them, and the fixer is handed work
no test can clear. As of 2026-09-05 that is vitest 5
([stryker-js#6210](https://github.com/stryker-mutator/stryker-js/issues/6210));
pin `vitest` to `^4` until a fixed runner ships. Measured: 12 Killed on
vitest 4, 12 Survived on vitest 5, same code and same tests.

guardrails detects this shape — a mutant reported `Survived` whose covering
tests never ran — and reports one `guardrails/analyzer-failed` naming it
instead of a storm of false survivors, so you will see it named rather than
chase it. But the pin is what gets you a working mutation gate. The same check
covers the rest of that runner's open false-survivor bugs, so keep it in mind
on any stryker or test-framework bump.

**Leave `thresholds.break` unset.** guardrails is the gate: it reads
stryker's report and raises one violation per surviving mutant, diff-scoped to
what this change touched. `break` asks a different question — a whole-project
Expand Down Expand Up @@ -187,14 +204,17 @@ right.
A stack verified together on 2026-09-04, on top of the Vite `react-ts`
template — a worked example, not a contract:

| package | range | why |
| ----------------------- | ------ | ----------------------------------- |
| `eslint` | `^10` | |
| `typescript` | `~6.0` | must be <6.1 — typescript-eslint |
| `typescript-eslint` | `^8` | sets the TypeScript ceiling |
| `@eslint/js` | `^10` | |
| `eslint-plugin-unicorn` | `^74` | current major needs `eslint >=10.4` |
| `eslint-plugin-sonarjs` | `^4` | |
| package | range | why |
| -------------------------------- | ------ | --------------------------------------- |
| `eslint` | `^10` | |
| `typescript` | `~6.0` | must be <6.1 — typescript-eslint |
| `typescript-eslint` | `^8` | sets the TypeScript ceiling |
| `@eslint/js` | `^10` | |
| `eslint-plugin-unicorn` | `^74` | current major needs `eslint >=10.4` |
| `eslint-plugin-sonarjs` | `^4` | |
| `vitest` | `^4` | vitest 5 breaks the stryker runner |
| `@stryker-mutator/core` | `^10` | fine on vitest 4; the runner is the pin |
| `@stryker-mutator/vitest-runner` | `^10` | see stryker-js#6210 above |

Note what that template gives you and what it does not: it pins TypeScript
itself (inside the ceiling, as of this writing) and it ships **oxlint**, not
Expand Down
Loading