@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.
Files changed (44) hide show
  1. package/README.md +155 -72
  2. package/dist/{build-GKWNLU4Q.js → build-GCUFPPIB.js} +11 -11
  3. package/dist/check-AKYABBIV.js +1459 -0
  4. package/dist/{chunk-CWF6QXEW.js → chunk-2UVLKBLV.js} +1 -1
  5. package/dist/chunk-34VGKPEM.js +59 -0
  6. package/dist/{chunk-KCEGA56S.js → chunk-BNACT47L.js} +39 -119
  7. package/dist/{chunk-ACTY42RE.js → chunk-C3NZIFW6.js} +66 -0
  8. package/dist/{chunk-CN25SRKD.js → chunk-EFCMBBBT.js} +71 -109
  9. package/dist/chunk-H55GYSQ6.js +102 -0
  10. package/dist/{chunk-MT4BOAAV.js → chunk-HL65IPEQ.js} +2 -2
  11. package/dist/{chunk-MAK3HG46.js → chunk-JEJRXPCU.js} +3 -3
  12. package/dist/chunk-LUTP2TVU.js +94 -0
  13. package/dist/{chunk-DMDAJPFJ.js → chunk-MKUYZIXF.js} +1 -1
  14. package/dist/{chunk-SGP5JX6I.js → chunk-MNYKLWRC.js} +1 -1
  15. package/dist/{chunk-YII4TAKS.js → chunk-NJSTAWDM.js} +3 -3
  16. package/dist/{chunk-RJBFCURK.js → chunk-NOC53Z5Q.js} +1 -1
  17. package/dist/{chunk-H5C4SE73.js → chunk-O6FCFZDF.js} +6 -63
  18. package/dist/{chunk-RSIOYQOQ.js → chunk-OON6O4S5.js} +34 -0
  19. package/dist/{chunk-7NC7AO75.js → chunk-TPMHY4L6.js} +15 -17
  20. package/dist/{chunk-IFFZ46YS.js → chunk-TRUK3TJF.js} +1 -1
  21. package/dist/chunk-WTXWEEI2.js +21 -0
  22. package/dist/{chunk-DSQDMFQ6.js → chunk-WULBOSG5.js} +6 -1
  23. package/dist/{chunk-NFFXYYM6.js → chunk-XXMD4NC5.js} +4 -4
  24. package/dist/cli.js +41 -17
  25. package/dist/{diff-A3TQWWUJ.js → diff-ST5KHT7P.js} +5 -5
  26. package/dist/{doctor-6ONS6LKY.js → doctor-OEIY4Y5X.js} +39 -6
  27. package/dist/{env-XF2US3CB.js → env-EQAOZCFR.js} +8 -9
  28. package/dist/{git-W6N3WMYQ.js → git-VGHUF2SY.js} +7 -1
  29. package/dist/{impact-DPZIAJR4.js → impact-AXLZTLVT.js} +20 -17
  30. package/dist/index.js +2 -2
  31. package/dist/{init-VNK5E4H3.js → init-QCOZWLOO.js} +27 -11
  32. package/dist/{insight-LNYVMCAE.js → insight-KRWT6Z47.js} +9 -10
  33. package/dist/{pr-6CYMM2XU.js → pr-7JXUC5BQ.js} +24 -21
  34. package/dist/{run-44PKMOV5.js → run-UOPS5FRU.js} +11 -11
  35. package/dist/stryker-host.d.ts +2 -0
  36. package/dist/stryker-host.js +135 -0
  37. package/dist/stryker-plugins.d.ts +33 -0
  38. package/dist/stryker-plugins.js +142 -0
  39. package/dist/{symbol-graph-OB4AJCLW.js → symbol-graph-GYOI4GU4.js} +3 -3
  40. package/dist/vitest.js +7 -5
  41. package/dist/{workspace-PIQXG4ZD.js → workspace-IQFLR3RY.js} +5 -5
  42. package/package.json +5 -2
  43. package/dist/check-KO5VY67Z.js +0 -44
  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 a change to a TypeScript repository: what
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
- It reads the diff at the level of the syntax tree, not the line, follows every
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
- - how risky each changed file is, and why;
18
- - which test files reach the change, with the import chain from each test to
19
- the file that changed;
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
- You set it up once. After that nobody types a variant command: your own test
24
- command and your pull requests are the interface.
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
- npx @blzsky/variant init
45
+ npm install -D @blzsky/variant@canary
28
46
  ```
29
47
 
30
- `init` finds out how the repository installs, tests and runs CI, shows every
31
- change it would make as a diff, and makes them when you agree. It adds one
32
- line to your Vitest config and one workflow that keeps a comment on each pull
33
- request.
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
- It does not skip tests. Your full suite runs as it always did; in CI variant
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
- variant: predicted 8 of 55 test files (high). 2 failed, all predicted.
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
- Leaving tests out is something variant has to earn in each repository, from
44
- that record, and is not built yet.
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
- > **Status**: `0.x`. What is described here is published as a canary,
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
- ## Install
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
- variant needs Node 20 or newer and a git repository.
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
- ### Upgrade
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
- ## Benchmarks
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
- ## Limitations
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-7NC7AO75.js";
6
- import "./chunk-IHXJ72XA.js";
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-CN25SRKD.js";
13
- import "./chunk-V5CTGXMM.js";
14
- import "./chunk-RJBFCURK.js";
10
+ } from "./chunk-EFCMBBBT.js";
11
+ import "./chunk-H55GYSQ6.js";
15
12
  import "./chunk-GDSZFIHM.js";
16
- import "./chunk-DMDAJPFJ.js";
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-RSIOYQOQ.js";
22
- import "./chunk-ACTY42RE.js";
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) {