@blzsky/variant 0.3.0-canary.0 → 0.3.0-canary.1
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 +155 -72
- package/dist/{build-GKWNLU4Q.js → build-GCUFPPIB.js} +11 -11
- package/dist/check-AKYABBIV.js +1459 -0
- package/dist/{chunk-CWF6QXEW.js → chunk-2UVLKBLV.js} +1 -1
- package/dist/chunk-34VGKPEM.js +59 -0
- package/dist/{chunk-KCEGA56S.js → chunk-BNACT47L.js} +39 -119
- package/dist/{chunk-ACTY42RE.js → chunk-C3NZIFW6.js} +66 -0
- package/dist/{chunk-CN25SRKD.js → chunk-EFCMBBBT.js} +71 -109
- package/dist/chunk-H55GYSQ6.js +102 -0
- package/dist/{chunk-MT4BOAAV.js → chunk-HL65IPEQ.js} +2 -2
- package/dist/{chunk-MAK3HG46.js → chunk-JEJRXPCU.js} +3 -3
- package/dist/chunk-LUTP2TVU.js +94 -0
- package/dist/{chunk-DMDAJPFJ.js → chunk-MKUYZIXF.js} +1 -1
- package/dist/{chunk-SGP5JX6I.js → chunk-MNYKLWRC.js} +1 -1
- package/dist/{chunk-YII4TAKS.js → chunk-NJSTAWDM.js} +3 -3
- package/dist/{chunk-RJBFCURK.js → chunk-NOC53Z5Q.js} +1 -1
- package/dist/{chunk-H5C4SE73.js → chunk-O6FCFZDF.js} +6 -63
- package/dist/{chunk-RSIOYQOQ.js → chunk-OON6O4S5.js} +34 -0
- package/dist/{chunk-7NC7AO75.js → chunk-TPMHY4L6.js} +15 -17
- package/dist/{chunk-IFFZ46YS.js → chunk-TRUK3TJF.js} +1 -1
- package/dist/chunk-WTXWEEI2.js +21 -0
- package/dist/{chunk-DSQDMFQ6.js → chunk-WULBOSG5.js} +6 -1
- package/dist/{chunk-NFFXYYM6.js → chunk-XXMD4NC5.js} +4 -4
- package/dist/cli.js +41 -17
- package/dist/{diff-A3TQWWUJ.js → diff-ST5KHT7P.js} +5 -5
- package/dist/{doctor-6ONS6LKY.js → doctor-OEIY4Y5X.js} +39 -6
- package/dist/{env-XF2US3CB.js → env-EQAOZCFR.js} +8 -9
- package/dist/{git-W6N3WMYQ.js → git-VGHUF2SY.js} +7 -1
- package/dist/{impact-DPZIAJR4.js → impact-AXLZTLVT.js} +20 -17
- package/dist/index.js +2 -2
- package/dist/{init-VNK5E4H3.js → init-QCOZWLOO.js} +27 -11
- package/dist/{insight-LNYVMCAE.js → insight-KRWT6Z47.js} +9 -10
- package/dist/{pr-6CYMM2XU.js → pr-7JXUC5BQ.js} +24 -21
- package/dist/{run-44PKMOV5.js → run-UOPS5FRU.js} +11 -11
- package/dist/stryker-host.d.ts +2 -0
- package/dist/stryker-host.js +135 -0
- package/dist/stryker-plugins.d.ts +33 -0
- package/dist/stryker-plugins.js +142 -0
- package/dist/{symbol-graph-OB4AJCLW.js → symbol-graph-GYOI4GU4.js} +3 -3
- package/dist/vitest.js +7 -5
- package/dist/{workspace-PIQXG4ZD.js → workspace-IQFLR3RY.js} +5 -5
- package/package.json +5 -2
- package/dist/check-KO5VY67Z.js +0 -44
- package/dist/chunk-QRPI6TJE.js +0 -68
package/README.md
CHANGED
|
@@ -8,55 +8,146 @@
|
|
|
8
8
|
|
|
9
9
|
## What is variant?
|
|
10
10
|
|
|
11
|
-
variant answers one question about
|
|
12
|
-
has to be verified before this is safe to merge?
|
|
11
|
+
variant answers one question about the code you just changed:
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
import across workspace packages, and says, on the pull request:
|
|
13
|
+
> **Your tests pass. Would they notice if this code were wrong?**
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- which typecheck, build and lint commands apply to the packages it affects;
|
|
21
|
-
- which changed files no test reaches at all.
|
|
15
|
+
`variant check` makes small, deliberate mistakes in the lines you changed, one
|
|
16
|
+
at a time, runs your tests against each, and lists the mistakes nobody
|
|
17
|
+
noticed:
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
```
|
|
20
|
+
Undetected: 1
|
|
21
|
+
src/discount.ts:3:9 in qualifiesForDiscount
|
|
22
|
+
- total >= 100
|
|
23
|
+
+ total > 100
|
|
24
|
+
Changing >= to > went undetected. Review the behavior at total = 100.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The tests checked 50 and 150 and passed with either operator. Add a test at
|
|
28
|
+
100, run it again, and the same mistake is caught. Each finding is a concrete
|
|
29
|
+
change to your code, not a score.
|
|
30
|
+
|
|
31
|
+
It works on the first run, with no history and no config. It looks at what you
|
|
32
|
+
have not committed yet, staged or not, and leaves your working tree exactly as
|
|
33
|
+
it was.
|
|
34
|
+
|
|
35
|
+
> **Status**: `0.x`. `variant check` is published as a canary,
|
|
36
|
+
> `0.3.0-canary.1`, under the `canary` tag: install `@blzsky/variant@canary`.
|
|
37
|
+
> Without the tag npm serves 0.2.1, which has no `check`. Any minor release
|
|
38
|
+
> can break, and every break is in the [CHANGELOG](./CHANGELOG.md).
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
variant needs Node 22 or newer, a git repository, and Vitest (4.1 is tested; 5.0 has been tried).
|
|
25
43
|
|
|
26
44
|
```bash
|
|
27
|
-
|
|
45
|
+
npm install -D @blzsky/variant@canary
|
|
28
46
|
```
|
|
29
47
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
Install it in the package whose code you change, beside its Vitest; the Vitest
|
|
49
|
+
it runs your tests with is yours.
|
|
50
|
+
|
|
51
|
+
The package is `@blzsky/variant`; the binary it installs is `variant`.
|
|
52
|
+
`npx variant` fetches an unrelated package, so always use the scope with `npx`.
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
predicts beside it which test files the change needs, then checks that
|
|
37
|
-
prediction against what failed and prints one line:
|
|
54
|
+
### Upgrade
|
|
38
55
|
|
|
56
|
+
```bash
|
|
57
|
+
npm install -D @blzsky/variant@latest
|
|
39
58
|
```
|
|
40
|
-
|
|
59
|
+
|
|
60
|
+
Because variant is `0.x`, read the CHANGELOG before moving to a new minor
|
|
61
|
+
version.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx @blzsky/variant check # before you commit
|
|
67
|
+
npx @blzsky/variant check --base main # everything this branch changes
|
|
68
|
+
npx @blzsky/variant check --fail-on-survivors --json # in CI
|
|
41
69
|
```
|
|
42
70
|
|
|
43
|
-
|
|
44
|
-
|
|
71
|
+
Run it from the package whose code changed, where its Vitest config is.
|
|
72
|
+
|
|
73
|
+
- **What it compares against.** With uncommitted changes, HEAD: your staged
|
|
74
|
+
and unstaged edits. On a clean branch, the commit it left the default branch
|
|
75
|
+
at. On GitHub Actions, the pull request's merge base, or the commit a push
|
|
76
|
+
replaced. When none of these applies it stops and asks for `--base` instead
|
|
77
|
+
of guessing.
|
|
78
|
+
- **What it mutates.** Comparisons (`>=` to `>`, `===` to `!==`), boolean
|
|
79
|
+
conditions (`&&` to `||`, a condition forced to `true` or `false`) and
|
|
80
|
+
arithmetic (`+` to `-`), in the changed lines of TypeScript source files,
|
|
81
|
+
and all of a new file you have not `git add`ed yet. Test files, declaration
|
|
82
|
+
files and files whose change is only formatting or comments are not
|
|
83
|
+
mutated; the report lists them and why.
|
|
84
|
+
- **First, the tests must pass as they are.** If one fails, variant stops: a
|
|
85
|
+
failure could not then be told apart from one a mutation caused.
|
|
86
|
+
- **What it reports.** Each mutation the tests did not notice, with the exact
|
|
87
|
+
change and what to review; code no test executed, listed apart; timeouts and
|
|
88
|
+
errors as inconclusive; what was not run and why; how many tests ran; and how
|
|
89
|
+
long it took. When every mutation is detected it says so, and says that this
|
|
90
|
+
is evidence the tests check those lines, not proof the change is correct.
|
|
91
|
+
- **Bounded.** At most `--max-mutants` mutations (default 50, the same ones
|
|
92
|
+
every run) within `--budget` (default 5 minutes, the first test run
|
|
93
|
+
included). A run that stops early says `Incomplete` and lists what it did
|
|
94
|
+
not run. It never reads as clean.
|
|
95
|
+
- **Advisory.** It exits 0 whatever it finds. `--fail-on-survivors` exits 1
|
|
96
|
+
when a mutation went undetected or unreached, or the run is incomplete.
|
|
97
|
+
|
|
98
|
+
The [`check` guide](./docs/check.md) has every flag, the CI setup, and how to
|
|
99
|
+
read a result. [`examples/check-demo`](./examples/check-demo) is the example
|
|
100
|
+
above, ready to run.
|
|
45
101
|
|
|
46
|
-
|
|
47
|
-
> `0.3.0-canary.0`, under the `canary` tag: write `@blzsky/variant@canary`
|
|
48
|
-
> wherever this page says `@blzsky/variant`. Without the tag npm serves
|
|
49
|
-
> 0.2.1, which has `impact`, `pr check`, `pr report` and `workspace check`,
|
|
50
|
-
> an `init` that only writes a task-runner config, and no adapter. The
|
|
51
|
-
> adapter is for Vitest; a Jest repository gets the pull request report
|
|
52
|
-
> only. Test skipping is **not implemented**, deliberately.
|
|
53
|
-
> The task runner underneath (`build`, `run`, `insight`) works but is frozen.
|
|
54
|
-
> Any minor release can break, and every break is in the
|
|
55
|
-
> [CHANGELOG](./CHANGELOG.md).
|
|
102
|
+
## How it compares
|
|
56
103
|
|
|
57
|
-
|
|
104
|
+
**Mutation testing is not new.** [StrykerJS](https://stryker-mutator.io/)
|
|
105
|
+
mutates JavaScript and TypeScript and runs your tests against each mutant, has
|
|
106
|
+
an incremental mode that reuses earlier results, and can be pointed at a set of
|
|
107
|
+
files or lines. variant runs on Stryker; it does not replace it. What variant
|
|
108
|
+
adds is the workflow around it: it works out which lines changed, mutates only
|
|
109
|
+
those, keeps the run bounded, explains each surviving mutation in terms of the
|
|
110
|
+
line you wrote, and leaves your working tree alone. If you already run Stryker
|
|
111
|
+
over a whole project, you have the data `check` gives you, and more.
|
|
58
112
|
|
|
59
|
-
|
|
113
|
+
**Coverage is weaker evidence.** A line counts as covered when a test runs it,
|
|
114
|
+
whether or not anything checks the result. The example above is 100% covered.
|
|
115
|
+
|
|
116
|
+
## Limitations
|
|
117
|
+
|
|
118
|
+
- **A surviving mutation is a finding to review, not a bug.** Some mutations
|
|
119
|
+
change nothing a valid input can observe: `>=` and `>` agree everywhere a
|
|
120
|
+
value can never equal the boundary. variant does not detect those, and does
|
|
121
|
+
not promise to.
|
|
122
|
+
- **Detected is not verified.** All mutations detected means the tests that ran
|
|
123
|
+
noticed these particular mistakes. It says nothing about mistakes of other
|
|
124
|
+
kinds, or about lines that were not mutated.
|
|
125
|
+
- **Only three kinds of mistake.** Comparisons, conditions and arithmetic.
|
|
126
|
+
Strings, literals, removed calls and removed statements are not mutated.
|
|
127
|
+
- **Deleted lines cannot be mutated.** A change that only removes code gets no
|
|
128
|
+
mutation; the report says how many lines that was.
|
|
129
|
+
- **Mutations are not type-checked.** One that TypeScript would reject still
|
|
130
|
+
runs, because Vitest does not check types.
|
|
131
|
+
- **Code run in another process reads as unreached.** Coverage is measured in
|
|
132
|
+
the test process, so a test that drives a CLI or a server it starts does not
|
|
133
|
+
count as executing that code.
|
|
134
|
+
- **Flaky tests can count as detection.** A test that fails at random while a
|
|
135
|
+
mutation is in place is read as noticing it.
|
|
136
|
+
- **One Vitest project, run from its package.** Workspace roots, Vitest browser
|
|
137
|
+
mode and Jest are not supported yet.
|
|
138
|
+
|
|
139
|
+
## Pull request reports
|
|
140
|
+
|
|
141
|
+
variant also explains pull requests: one comment on each that says what the
|
|
142
|
+
change affects, which tests reach it and why, and what no test reaches. This is
|
|
143
|
+
what `variant init` sets up. It predates `check`, and works as it did.
|
|
144
|
+
|
|
145
|
+
### Setting up
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx @blzsky/variant init # shows the changes, asks, then applies
|
|
149
|
+
npx @blzsky/variant init --dry-run # only look
|
|
150
|
+
```
|
|
60
151
|
|
|
61
152
|
```bash
|
|
62
153
|
npx @blzsky/variant init # shows the changes, asks, then applies
|
|
@@ -73,16 +164,7 @@ with confidence it leaves alone and tells you the lines to add.
|
|
|
73
164
|
The package is `@blzsky/variant`; the binary it installs is `variant`.
|
|
74
165
|
`npx variant` fetches an unrelated package, so always use the scope with `npx`.
|
|
75
166
|
|
|
76
|
-
###
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
npm install -D @blzsky/variant@latest
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Because variant is `0.x`, read the CHANGELOG before moving to a new minor
|
|
83
|
-
version.
|
|
84
|
-
|
|
85
|
-
## Usage
|
|
167
|
+
### What you see
|
|
86
168
|
|
|
87
169
|
After `init` there is nothing to run. What you see:
|
|
88
170
|
|
|
@@ -130,7 +212,7 @@ follows the importers of the changed names for `pr check`'s verdict. A change
|
|
|
130
212
|
to `package.json`, a lockfile, `tsconfig*.json`, a test runner config or a test
|
|
131
213
|
setup file selects every test.
|
|
132
214
|
|
|
133
|
-
### In CI
|
|
215
|
+
### In CI, by hand
|
|
134
216
|
|
|
135
217
|
`init` writes what CI needs. To wire the commands in by hand: check out
|
|
136
218
|
with `fetch-depth: 0`; the base is detected on GitHub Actions, and elsewhere
|
|
@@ -165,32 +247,7 @@ branch names and numbers, no file paths and no source.
|
|
|
165
247
|
and they go into the measurement that decides whether skipping ever ships. The
|
|
166
248
|
[measurement log](./docs/measurement.md) has every number collected so far.
|
|
167
249
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Measured by CI on 2026-09-28 with [`benchmarks/bench.mjs`](./benchmarks/bench.mjs),
|
|
171
|
-
on a generated git repository of 300 TypeScript files with 25 changed. Each
|
|
172
|
-
number is the median of the runs shown. Machine: AMD EPYC 9V74 (4 cores),
|
|
173
|
-
16 GB RAM, Linux, Node 22.23.2, hyperfine 1.18.0.
|
|
174
|
-
|
|
175
|
-
| | Median | Runs |
|
|
176
|
-
|---|---:|---:|
|
|
177
|
-
| `variant --help` (startup) | 36.7 ms | 20 |
|
|
178
|
-
| `variant impact`, 25 changed files of 300 | 988 ms | 10 |
|
|
179
|
-
|
|
180
|
-
- **Startup is a CI gate.** The benchmark workflow fails when the `--help`
|
|
181
|
-
median passes 200 ms, because every command is loaded only when it runs.
|
|
182
|
-
- **The symbol index is cached.** `.variant/graph/symbols.json` keeps each
|
|
183
|
-
file's exports and imports keyed by content hash, so a later run re-parses
|
|
184
|
-
only the files whose content changed.
|
|
185
|
-
- **Large real repositories are not measured yet.** 300 generated files say
|
|
186
|
-
how the pipeline behaves, not what it costs on your monorepo. Whether the
|
|
187
|
-
analysis pays for itself depends on how long your suite takes, and that
|
|
188
|
-
number has to come from real runs.
|
|
189
|
-
|
|
190
|
-
Reproduce with `pnpm build && pnpm bench`; the methodology is in
|
|
191
|
-
[`benchmarks/README.md`](./benchmarks/README.md).
|
|
192
|
-
|
|
193
|
-
## How it compares
|
|
250
|
+
### How the reports compare
|
|
194
251
|
|
|
195
252
|
**Nx `affected`, Turborepo `--filter`.** These work at the level of projects
|
|
196
253
|
and tasks: a change inside `utils` reruns the test target of `utils` and of
|
|
@@ -209,7 +266,7 @@ file runs, whichever functions it calls. The exported-surface classification
|
|
|
209
266
|
decides `pr check`'s build verdict and whether a change selects anything, not
|
|
210
267
|
which functions a test touches.
|
|
211
268
|
|
|
212
|
-
|
|
269
|
+
### What the reports cannot see
|
|
213
270
|
|
|
214
271
|
Worth knowing before you trust a prediction. variant reads source statically,
|
|
215
272
|
so it sees imports and nothing else. It widens the selection or lowers its
|
|
@@ -256,10 +313,36 @@ cannot see at all:
|
|
|
256
313
|
The full list, with how each case is handled, is in
|
|
257
314
|
[Impact & workspace](./docs/impact-and-workspace.md#limitations).
|
|
258
315
|
|
|
316
|
+
## Benchmarks
|
|
317
|
+
|
|
318
|
+
Measured by CI on 2026-09-28 with [`benchmarks/bench.mjs`](./benchmarks/bench.mjs),
|
|
319
|
+
on a generated git repository of 300 TypeScript files with 25 changed. Each
|
|
320
|
+
number is the median of the runs shown. Machine: AMD EPYC 9V74 (4 cores),
|
|
321
|
+
16 GB RAM, Linux, Node 22.23.2, hyperfine 1.18.0.
|
|
322
|
+
|
|
323
|
+
| | Median | Runs |
|
|
324
|
+
|---|---:|---:|
|
|
325
|
+
| `variant --help` (startup) | 36.7 ms | 20 |
|
|
326
|
+
| `variant impact`, 25 changed files of 300 | 988 ms | 10 |
|
|
327
|
+
|
|
328
|
+
- **Startup is a CI gate.** The benchmark workflow fails when the `--help`
|
|
329
|
+
median passes 200 ms, because every command is loaded only when it runs.
|
|
330
|
+
- **The symbol index is cached.** `.variant/graph/symbols.json` keeps each
|
|
331
|
+
file's exports and imports keyed by content hash, so a later run re-parses
|
|
332
|
+
only the files whose content changed.
|
|
333
|
+
- **Large real repositories are not measured yet.** 300 generated files say
|
|
334
|
+
how the pipeline behaves, not what it costs on your monorepo. Whether the
|
|
335
|
+
analysis pays for itself depends on how long your suite takes, and that
|
|
336
|
+
number has to come from real runs.
|
|
337
|
+
|
|
338
|
+
Reproduce with `pnpm build && pnpm bench`; the methodology is in
|
|
339
|
+
[`benchmarks/README.md`](./benchmarks/README.md).
|
|
340
|
+
|
|
259
341
|
## Documentation
|
|
260
342
|
|
|
261
343
|
| | |
|
|
262
344
|
|---|---|
|
|
345
|
+
| [`check`](./docs/check.md) | Would your tests notice? Flags, CI, reading a result, limits |
|
|
263
346
|
| [Tutorial](./docs/tutorial.md) | Every command, step by step, on a sample monorepo |
|
|
264
347
|
| [Getting started](./docs/getting-started.md) | Run it on your own repository, then set up the task runner |
|
|
265
348
|
| [Impact & workspace](./docs/impact-and-workspace.md) | How `impact` and `workspace check` work, and what static analysis cannot see |
|
|
@@ -2,24 +2,24 @@ import {
|
|
|
2
2
|
executeTarget,
|
|
3
3
|
renderDryRunPlan,
|
|
4
4
|
reportRunInsights
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-CWF6QXEW.js";
|
|
8
|
-
import "./chunk-QRPI6TJE.js";
|
|
5
|
+
} from "./chunk-TPMHY4L6.js";
|
|
6
|
+
import "./chunk-2UVLKBLV.js";
|
|
9
7
|
import {
|
|
10
8
|
createContext,
|
|
11
9
|
toRunOptions
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-RJBFCURK.js";
|
|
10
|
+
} from "./chunk-EFCMBBBT.js";
|
|
11
|
+
import "./chunk-H55GYSQ6.js";
|
|
15
12
|
import "./chunk-GDSZFIHM.js";
|
|
16
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-MKUYZIXF.js";
|
|
14
|
+
import "./chunk-IHXJ72XA.js";
|
|
15
|
+
import "./chunk-V5CTGXMM.js";
|
|
17
16
|
import "./chunk-3E5LBU3O.js";
|
|
18
|
-
import "./chunk-YYGCSKIQ.js";
|
|
19
17
|
import "./chunk-6J4XDG44.js";
|
|
18
|
+
import "./chunk-NOC53Z5Q.js";
|
|
19
|
+
import "./chunk-YYGCSKIQ.js";
|
|
20
20
|
import "./chunk-EISTWXDW.js";
|
|
21
|
-
import "./chunk-
|
|
22
|
-
import "./chunk-
|
|
21
|
+
import "./chunk-OON6O4S5.js";
|
|
22
|
+
import "./chunk-C3NZIFW6.js";
|
|
23
23
|
|
|
24
24
|
// src/core/scope/task-filter.ts
|
|
25
25
|
function taskPackage(taskName) {
|