@systemfsoftware/stryker-js 17.0.2 → 23.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,194 @@
1
1
  # @systemfsoftware/stryker-js
2
2
 
3
+ ## 23.0.0
4
+
5
+ `effect` is a peer dependency (`^4.1.2`) of every package whose library entry points import it, so a consumer's schemas, layers and errors and these packages' share one effect instance.
6
+
7
+ - cli-contract, plugin-interface, instrumenter, typescript-checker and plugin-runtime declared `effect` as a dependency. A consumer on another 4.1.x got a second copy, and the contract's `ShardPlan` no longer type-checked against the consumer's effect. They now peer on it; install `effect` yourself.
8
+ - stryker-js and the html-reporter already peered on `effect`; the floor rises from 4.1.0 to 4.1.2, the version these packages are built and tested with.
9
+
10
+ The processes Stryker starts are unchanged: the CLI (`main.mjs`), the TypeScript checker's worker and the vitest runner's worker bundle their own effect 4.1.2. The flake package `stryker-package` now declares `effect` itself.
11
+
12
+ The install section names `pnpm add` and the npm packages instead of the flake. The workspace takes the released packages it dogfoods from npm; no published manifest changes.
13
+
14
+ ## 22.0.0
15
+
16
+ `run --since` and `plan --since` widen a project to a full run only when a Stryker config, a test-runner config, a `package.json` or a lockfile changed in that project or in one of its ancestor directories. A change to those files in a sibling package no longer widens it. The diff is read with its output format pinned (`--no-color`, `--no-ext-diff`, `--no-textconv`, `--no-relative` and the `a/`/`b/` prefixes), so a user's git config can no longer empty or misplace the diff scope.
17
+
18
+ Each call the CLI makes to a plugin worker is traced as `RpcClient.<method>` again, the name the span taxonomy declares. effect 4.0.0 used that prefix by default; later effect releases name the span after the method alone, so `RpcClient.check`, `RpcClient.mutantRun` and their siblings had stopped appearing.
19
+
20
+ The CLI ships as the flake's `stryker` package (`nix run github:systemfsoftware/stryker-js-effect/<release>#stryker`): `bin/stryker` with the vitest runner, the TypeScript checker, the instrumenter, the svelte framework and both Effect ignorers in one `lib/node_modules` tree. A project installs none of them; to type its `stryker.config.ts` it links `lib/node_modules/@systemfsoftware/stryker-js` from that package.
21
+
22
+ - A bare plugin name in `plugins`, `appendPlugins`, `ignorers`, `testRunner.plugin` or a checker's `plugin` now resolves from the CLI's own install, not from the project. A plugin the CLI does not ship is named by its `file:` URL, for example `import.meta.resolve('my-ignorer')` in the config.
23
+ - The package no longer declares a `bin`. Run `stryker` from the flake's package.
24
+
25
+ ## 21.0.0
26
+
27
+ A wall-clock expiry no longer scores a mutant as a detected Timeout on its own. When a mutant's run outlasts its deadline, the worker is replaced as before and the mutant runs once more in the same run, on the fresh worker, under twice the deadline. It is reported Timeout (`wall-clock-timeout`) only if that run expires too; otherwise it gets the verdict its tests give. If the fresh worker never starts the run, the mutant is a RuntimeError, not a Timeout. A hit-limit Timeout is unchanged.
28
+
29
+ Breaking: mutants that a slow run used to push over the deadline now report Survived or Killed, so mutation scores can drop. A confirmed Timeout is reused by the next incremental run instead of being refused as `timeoutUnreproduced`, and its recorded cost covers both attempts.
30
+
31
+ The repository's own mutation and test tooling now installs the v20.0.0 release tarballs; the published code and the dependencies a consumer installs are unchanged.
32
+
33
+ Every `Mutant` now carries a required `probeEligibility`: `Probed { kind }` when a dry run could compute the mutant's value from operands the original code already evaluates (`arithmetic`, `relational`, `unary` or `constant`), otherwise `Unprobed { reason }`. A mutant decoded or constructed without the field reads `Unprobed` with reason `not-decided`. The `Mutant` namespace exports the new schemas.
34
+
35
+ The instrumenter marks `Probed` the `-`, `*`, `/` and `%` arithmetic swaps, swaps of a `<`, `<=`, `>` or `>=` comparison, `+x`/`-x` sign flips, and `true`/`false` replacements of a comparison between two literals. Nothing reads the field yet: mutant statuses and the tests each mutant runs are unchanged.
36
+
37
+ Migration: code that builds a `Mutant` object literal must set `probeEligibility`.
38
+
39
+ `stryker plan --since <ref>` plans only the mutants on lines changed since a ref, recording scope `Unscoped`, `DiffScoped` (merge base, HEAD, file digest) or `FullScope`. `run --plan --shard` runs those mutants, exiting 2 at another HEAD, when a scheduled file changes, or a scheduled mutant is undiscoverable (a config edit), naming the plan and the missing ids; the plan file and unrelated edits never matter. `run --plan <file>` without `--shard` exits 2 naming `--shard` and the next action. Undecodable plans, unknown refs and failed git commands exit 2, naming `plan-undecodable` and next `stryker plan --since <base>`. `GitDiff` gains `head`, `toplevel` and `sourceDigest`; `changedSince` returns `hunks[].file` and `untrackedFiles` relative to the requested directory; `GitDiff.layer` requires `ChildProcessSpawner` and `Path.Path`; `GitDiffSchema` adds `PlannedDiff`.
40
+
41
+ Runs now plant AtomicUpdateSplit, SynchronizationRemoval and FinalizerEscape by default, so scores and the mutant set change. Naming one of them in mutator.optInMutations refuses the run as a config failure (default-tier mutator); remove it from that list, or move it to mutator.excludedMutations to keep it off. Their replacements use Effect 4 APIs and the peer range is effect ^4.0.0, so a repository on Effect 3 must list the three in mutator.excludedMutations.
42
+
43
+ ## 20.0.0
44
+
45
+ Refuse dry-run coverage recorded under another project root. Coverage now records the project root as the run spells it, the spelling mutant ids are computed from, and a run reuses it only under that same root: coverage from another root, including the same directory reached through a symlink, is refused with the new reason rootChanged, and coverage that records no root is refused once with rootUnrecorded. Either refusal runs a full dry run that records the current root, and its log line names the roots and the next action. Among several restored reports, only coverage recorded under the current root is reused, and only its flaky mutants force a dry run. `plan` prices from that coverage only when the run reuses it; otherwise it estimates from the first report recorded under the current root. Coverage is shared across machines only when they run from the same absolute workspace path.
46
+
47
+ Tests: the laws for the dropped-mutant check now pin when a dropped mutant pairs with its dominator in the kill matrix, and when the check fails.
48
+
49
+ A mutation run streams its events to the config's `progressStreamFile`; the flag still wins. Stream files there or at `reports/mutation-stream.jsonl` no longer make the next run re-run every mutant; output paths match with exact case where Stryker finds the file system tells case apart. An existing non-stream file at the stream path is kept and, like an unwritable one, keeps `--json` output.
50
+
51
+ Breaking:
52
+
53
+ - A non-empty config `progressStreamFile` moves a run's stream off `reports/mutation-stream.jsonl`.
54
+ - A config function is called once more per run: its first answer sets the stream path (else the flag or default), its second the rest, and a failing second call fails the run. Return the same options on every call.
55
+ - Your own `reports/mutation-stream.jsonl` is no longer a project input.
56
+ - `RunEvent.RunEventDrain.DefaultProgressStreamFile` is removed; read `Configuration.defaultOptions`.
57
+
58
+ Build on effect 4.1.0. effect 4.1.0 removed the `effect/Crypto` module, which the published stryker-js and stryker-js-vitest-runner imported, so a mutation run on effect 4.1.0 failed with ERR_MODULE_NOT_FOUND. Both packages now take random identifiers from `effect/crypto/SecureRandom` (`NodeCrypto.layerSecureRandom`), and the effect peer range is now `^4.1.0`: `effect/crypto/*` does not exist in 4.0.x. Schema classes are branded as @effect/tsgo 0.63.0 requires, the MCP server registers its toolkit through the stdio transport's `registrations` option with a middleware that provides the engine services to each request, and file identity in the case-sensitivity probe is a bigint, as effect 4.1.0 reports it.
59
+
60
+ Breaking: two public Layer members are renamed and one export is removed, so every service module hands out Layers only as members of its Service class, under the pack's names. `Engine.RunEnvironment.stage` is now `Engine.RunEnvironment.layerStage`, with the same two call signatures and the same `Layer<RunStageServices, never, EnginePorts>`. `RunEvent.RunEventDrain.fileLayer` is now `RunEvent.RunEventDrain.layerFile`. `RunEvent.RunEventDrainLive` is removed: use `RunEvent.RunEventDrain.layer`, which it aliased. Behaviour is unchanged.
61
+
62
+ `stryker gate` now fails when a project's mutation score is below that project's own `thresholds.break`, as an unsharded run already did. `stryker merge` records each project's thresholds and files under a new `projects` field, so a merged sharded run fails where the unsharded run would; before, it passed.
63
+
64
+ A failing `stryker gate` exits `1` with reason code `score-below-break`: a count line, one line per failing project with its score and break, then the next action. It runs after the baseline and budget checks. A project with a break and no valid mutant, or whose shards recorded no thresholds, gets an info line instead of a verdict. A report with no `thresholds` is refused as unusable input.
65
+
66
+ Migration: raise the score, set each project's own `thresholds.break` to its current score, or set it to `null`.
67
+
68
+ The built-in reporters are one capability: each reporter declares its name, whether it writes to stdout, and its failure mapping once, and the registry is one list. The SARIF reporter now runs as its own cell, recorded under the `stryker.report.sarif` span. Reporter output, report files, exit codes and the stream are unchanged.
69
+
70
+ The `--ignorePatterns` help now lists the patterns Stryker actually always ignores: `node_modules`, `.git`, `*.tsbuildinfo`, `/stryker.log`, `.next`, `.nuxt`, `.svelte-kit` and `.turbo`. It also names Stryker's own output files, which are always excluded: the files at `tempDirName`, `incrementalFile`, `progressStreamFile`, `reports/mutation-stream.jsonl`, `htmlReporter.fileName` and `jsonReporter.fileName`, the SARIF report and the reproducers file. The old help said `/reports` was always ignored, but it never was: other files under `reports/`, such as coverage output, are part of your project. If you set `ignorePatterns: []` relying on the old help, add a pattern for any other `reports/` content you want left out.
71
+
72
+ Incremental runs now reuse results for test files that import `#` subpath imports or Node builtins. A `#` import resolves through the nearest package manifest's `imports` map, `@systemfsoftware/source` condition first, and follows a target that names a builtin or another package. `node:*` and Node's builtin names are external. Before, either import made any edit invalidate the test file.
73
+
74
+ An `imports` entry Stryker cannot read (`null`, an array, nested conditions) now leaves only its own `#` import unresolved; `exports` and `main` still resolve.
75
+
76
+ `exports` patterns match as in Node: exact key, then longest prefix, so `./features/*` resolves and beats `./*`. Wildcards insert the matched text literally (`$&` is no longer a replacement pattern). A target with a `.`, `..` or `node_modules` segment after `./` is invalid and stays unresolved.
77
+
78
+ The `instrument` phase now covers instrumentation and the sandbox copy. A run used to enter `instrument` only after both had finished, so `phaseDurations.instrument` read close to 0 ms and their time counted under `prepare`. The `instrument` phase event and its `instrument` trace span now start before the files to mutate are read and instrumented, for mutation runs and for plans. `phaseDurations.instrument` grows by the instrumentation and sandbox time, and `prepare` shrinks by the same amount. A run that fails while instrumenting or building its sandbox now reports the `instrument` phase as entered.
79
+
80
+ Incremental runs keep reusing verdicts for a test that imports a linked workspace package whose manifest declares `exports` with nested conditions, array fallbacks, root conditions without a `.` key or `null` targets. Such a manifest no longer counts as unreadable as a whole: each `exports`, `imports`, `main` and `module` entry is read on its own, and an entry that maps to no file still keeps the closures that import it open.
81
+
82
+ A condition object or array fallback now resolves as Node resolves it, skipping what Node skips and refusing what Node refuses, and closes its importers' closures only when ESM and CommonJS importers load the same file. A `types`-first or dual `import`/`require` package no longer keeps a stale verdict on a test runner that does not report loaded modules.
83
+
84
+ The README has a new Known issues entry: on Node 24 (and 26 up to at least 26.7.0), `JSON.parse` can return the wrong object key in a long-lived process that reads Stryker report JSON, tracked upstream as nodejs/node#63785.
85
+
86
+ Tests: the lockfile scenarios now run exactly the projects the repository grades, so the two cannot drift apart.
87
+
88
+ The run-inputs digest now hashes only the mutated project's own dependency closure from `pnpm-lock.yaml` (its importer, its ancestor importers, linked workspace importers, and the snapshots and package entries they reach) plus the hoisting settings and `packageManager`, so a lockfile edit outside a project's dependencies keeps its saved verdicts. Other lockfiles, and a `pnpm-lock.yaml` the digest cannot use, still hash the whole file, and the unusable case logs one warning naming the reason. Canonical JSON now orders keys by code unit. The incremental cache version moves to 6, so the first run after upgrading re-tests every mutant once.
89
+
90
+ The verdict on the mutation score now runs inside its own span, `stryker.mutationReporting.judgeScore`, which the span taxonomy declares. The verdict's log lines are recorded as events under that span instead of directly on `stryker.mutationReporting.reportAll`. Exit codes and log text are as before: a score below `thresholds.break` exits 1, a score equal to it passes, and a reporter that fails on the final report still exits 3 when the score is also below the threshold.
91
+
92
+ No change to the published packages. The repository's own mutation run of these three packages now grades workflow files only, not schema files.
93
+
94
+ Property-test generators for planMutantTests now draw only valid mutants.
95
+
96
+ A `plan` whose `--projects` names one project directory twice, whether by a second path such as `proj-a,./proj-a` or through a symlink, now plans that project once and writes the same plan as naming it once. Before, it planned that project's mutants twice. When two equally costly mutant groups from different projects compete for a shard, the plan now orders them by project name first, so it places them the way the documented longest-processing-time rule does.
97
+
98
+ A `plan` the planner cannot accept, such as `--target-seconds 0`, now exits with code 2 and names the refused input and what to change, instead of crashing.
99
+
100
+ Breaking: `Engine.planRequest` can now fail with `PlanShardsRefused`, which `Engine` exports.
101
+
102
+ `stryker plan` and the MCP server's `rerun_mutant` tool behave as before; nothing you import or configure changes.
103
+
104
+ `stryker audit` refuses a report whose `files` record holds an empty file name or one containing NUL. The `matrix-undecodable` error names the key; it used to count the entry. On Node 24 and Node 26, it also reads every file name in a report exactly, even after an earlier report in the same run held a file name ending in a backslash (nodejs/node#63785).
105
+
106
+ Nothing you run changes: a mutation report reads, and is refused, exactly as before.
107
+
108
+ Tests: the workspace-package reuse scenarios read the whole machine-mode output through the published stream codec.
109
+
110
+ The sandbox is acquired the same way as before: in place or copied, with the same backup restore, build command and node_modules links. `stryker.sandbox.build.run` and `stryker.sandbox.symlink_node_modules` still sit inside `stryker.sandbox.acquire`, the symlink span and its "Start symlink node_modules" debug line still appear on every run, and an in-place run still restores its original files when the sandbox closes. The copy and preprocessing step inside the acquisition now has its own span, `stryker.sandbox.prepare`, which the span taxonomy declares.
111
+
112
+ Runs that share one `tempDirName` directory no longer break each other's sandboxes. A finishing run removes its own sandbox, then removes the shared directory only if it is empty at that moment. A run that starts while another removes the shared directory creates it again and continues.
113
+
114
+ `Engine.EnginePorts` now includes a service that removes the shared directory only when it is empty. Only `Engine.nodePlatformLayer` provides it, so runs under that layer need no change, and code that builds the engine's ports from separate layers must build them from `Engine.nodePlatformLayer` instead.
115
+
116
+ Rebuilt against updated workspace packages; nothing you import or configure changes.
117
+
118
+ No change for users: checker plugins are still called the same way.
119
+
120
+ No change to the published package. Two of its tests now say where the runner they import is declared.
121
+
122
+ ## 19.0.0
123
+
124
+ The verdict line's `phaseDurations` gains `check` and `reporting`. `check` is checker-busy wall-clock (the union of the intervals in which any checker was starting or checking); it overlaps the other phases, and a run with no checker configured reports `{ "_tag": "not-run" }` rather than zero. `reporting` runs from the end of mutant execution to the verdict, so `mutation-test` now stops where `reporting` starts, and the five sequential phases add up to the elapsed time.
125
+
126
+ Breaking:
127
+
128
+ - The stream emits a `phase` event for `reporting`, so `RunPhase` gains that value; exhaustive matches over `RunPhase` must handle it.
129
+ - `mutation-test` no longer includes reporting time.
130
+ - Verdict lines written before this release decode both new members as `{ "_tag": "not-recorded" }`. `Checker.checkGroupedPlans` is replaced by `Checker.checkGroupedCell`. Call `Checker.checkGroupedCell.run({ checker, checkerName, plans })` where you called `Checker.checkGroupedPlans(checker, checkerName, plans)`. It groups the plans with the checker, checks each group, and returns the same results and errors as before. Under the default `mutator.mutantSetPolicy`, a relational comparison in an `if`, loop, or ternary test no longer has its mutants Ignored by the sufficient-set table. That table dropped the complementary operator and one boolean literal at every such site, but its proof assumes numeric operands, which JavaScript does not guarantee. In `if (a < b)` with `a` null and `b` undefined, the dropped `true` mutant enters the branch and no kept mutant does, so a test that killed only that mutant went uncounted. Operands whose `valueOf` has side effects defeat the dropped operators the same way. Those mutants now run.
131
+
132
+ Breaking: default runs plant more active mutants in condition positions, so mutation scores and the set of mutants that run differ from the previous release. The generated mutants and their ids are unchanged. No consumer-visible change: the counts-only report's ignored-by-rule totals are unreleased, and they still behave as their own entry describes. The incremental cache now identifies the engine by a digest of its installed files instead of a hand-bumped integer. The digest covers the manifest and every file under the paths the `files` field declares, for `@systemfsoftware/stryker-js` and the runner it depends on, as resolved at run time. A verdict written by any other build of the engine is refused with the `semanticsChanged` reason, so a release that changes what a status means no longer depends on someone remembering to bump a constant. A declared path that is missing on disk stops the run instead of being hashed as absent.
133
+
134
+ Breaking: the incremental report replaces `verdictSemanticsVersion` (an integer) with `engineDigest` (a string), and `incrementalVersion` moves to `'4'`. Reports written by earlier releases are discarded with `cacheLayoutChanged` and the next run is a full run. Every machine-stream mutant line now carries `statusReason`. An Ignored line names the rule that removed it as `<rule-id>: <detail>` (for example `arid-logging: Effect.logInfo`); other statuses carry their note or `null`. `stryker merge` keeps the reason in the JSON report.
135
+
136
+ Breaking:
137
+
138
+ - The stream `schemaVersion` is now `8.0`, which also adds the `subsumption` key. A mutant line without `statusReason`, or an Ignored line whose reason names no known rule, is refused. `stryker merge` refuses a shard stream of another major version, naming both versions.
139
+ - `RunEvent.RunMutantTestedEvent` carries `statusReason`. Building or decoding an Ignored event whose reason names no ignore rule fails.
140
+ - `RunEvent.RunEvent` is a tagged union keyed by `_tag`: use its `cases`, `guards` and `match` to handle each event kind. `Engine.Project` is now read-only. Its `files` and `filesToMutate` are `ReadonlyMap<string, ProjectFile>` instead of `MutableHashMap`, so read them with `.get`, `.keys()`, `.values()` and `.size`. `ImportClosure.analyzeImportClosure` now needs a `SourceParser` service, and `Engine.EnginePorts` includes it. `Engine.nodePlatformLayer` provides it, so code that runs the analysis or the engine under that layer needs no change. Code that builds the engine's ports itself must add a `SourceParser` layer. The new `SourceParser` namespace exports the service and its shape.
141
+
142
+ Import closure analysis gives the same closures, `open` flags and digests as before. A source file holding a regex or bigint literal is parsed as it was. Tests: a deferred shard holding an already-settled mutant runs the dry run; the checker-accepts fallback compares status reasons; requireDryRun laws draw non-empty mutant lists. Plugins that ignore a mutant must now say why. Each ignore rule id (`Mutant.IgnoreRuleId`) is documented in the schema with what it means and the option that keeps its mutants; the ids are stable.
143
+
144
+ Breaking:
145
+
146
+ - A checker answering `ignored` must give a reason of the form `<rule-id>: <detail>`, or the run fails naming the checker and the mutant. A third-party checker with no rule of its own uses the new `checker` rule: `checker: <detail>`.
147
+ - An ignorer whose `shouldIgnore` returns an empty or non-string reason now fails instrumentation, naming the ignorer, the mutant id, and the file. Return a non-empty string to ignore a mutant, or `undefined` to keep it. A subsumed mutant (the Ignored complement of a kept ordering mutant, e.g. `a >= b` beside `a <= b`) now runs again whenever none of the dominators named in its `Subsumed` reference runs: a dominator the checker ignores, a dominator that fails to compile, a dominator ignored at plan time, or a remembered dominator that did not run. It is then checked and executed like any other mutant, and its `subsumption` becomes a `Readmitted` reference naming each dominator and why it did not run. While a named dominator does run, the mutant stays Ignored and its status reason names the dominator. A mutant already Ignored for another reason (for example `ignoreStatic`) keeps that reason.
148
+
149
+ A shard plan keeps each subsumed mutant in the same shard as its first dominator, without raising the shard count above `maxShards`. A verdict reused from the incremental record keeps the reason it was recorded with: a reused Ignored mutant still names its rule on the stream, in the JSON report, and in the next record, and a reused Timeout keeps `wall-clock-timeout`. Whether a verdict was reused is now the record's `remembered` field, no longer a `Remembered` status reason.
150
+
151
+ Breaking: `incrementalVersion` is now `'5'`. Records from earlier releases are discarded with `cacheLayoutChanged` and the next run is a full run. Read `remembered === true` where you matched `statusReason === 'Remembered'`. New `stryker audit` subcommand. With `--matrix <dir>` it reads a run recorded with every killer kept (`STRYKER_KILL_MATRIX=1`), finds every mutant the default mutant-set policy drops without running a test, and judges each against its dominator: `Pass`, `Vacuous`, `AttributionUnverified` or `Fail`, each with a reason code and a next action. A test whose every killed mutant is dropped is reported as orphaned. `--files` limits the check to the named files, so a pull request checks only what it changes. It exits `1` when a pair fails, a rule has no joined pair, or a test is orphaned, and `2` on unusable input. `--counts-only` counts planned, `CompileError`, compiled and executed mutants and test executions per project from finished reports. Both modes write versioned JSON to `--out` and print at most 20 detail lines. **Breaking:** the run stream moves to schema version `8.0`. Every `mutant` line has a required `subsumption` key: `null`, a `Subsumed` reference (rule and dominator ids) on an Ignored line, or a `Readmitted` reference (rule, each dominator and its cause code) on a line that is not Ignored. A line that lacks the key or whose reference disagrees with its status is refused. `stryker merge` refuses a `7.0` shard stream and keeps `subsumption` in the merged report. Read the key, or set `mutator: { mutantSetPolicy: 'full' }` to get only `null`.
152
+
153
+ The incremental report keeps the reference on each record, and a record carrying one is never remembered: it counts under the new reuse refusal `decidedPerRun`.
154
+
155
+ Subsumed mutants are Ignored, so the mutation score covers only the kept mutants and differs from a `full`-policy run.
156
+
157
+ ## 18.1.0
158
+
159
+ Only the package manifest changes: the development dependencies on stryker-js, its runner, its checker and the two ignorers now name the released tarballs instead of the npm `latest` tag. Runtime code, dependencies and peer dependencies are unchanged. The TypeScript checker now resolves a tsconfig `extends` that names a package subpath through that package's `exports` map, as TypeScript does. Before, the lookup only tried the literal path under `node_modules`; a package that maps the subpath elsewhere made the checker report no program digest, so no CompileError verdict in that project could be reused.
160
+
161
+ `stryker plan` now asks the checker for the program digest when the previous incremental report holds a CompileError verdict, and reuses those verdicts on the same terms as the run. Before, the plan always passed no digest and scheduled every CompileError mutant. With `inPlace: true` the plan still schedules them, because an in-place run digests the instrumented files. A checker that cannot produce a digest now logs a warning naming the checker, the project and the reason instead of failing silently. `Engine` exports `planRequest`. A run now skips the initial test run when every mutant it holds was a CompileError last time and a checker is configured. The checkers settle those mutants first; the test run starts only if a checker accepts one of them, and that mutant is then scored exactly as a forced run scores it. A shard holding only CompileError mutants of a project no longer pays that project's dry run.
162
+
163
+ `stryker plan` now groups the mutants that need a project's dry run onto as few shards as the target allows, and each shard's `predictedSeconds` includes the dry runs that shard will execute fresh. A dry run the shard can reuse from the incremental report is priced at zero. Before, every shard holding any mutant of a project ran that project's dry run, and the plan did not count it.
164
+
165
+ ## 18.0.0
166
+
167
+ A dry run now records the modules each test file evaluated, and a later incremental run uses them. A test file that loads a module only through a computed dynamic `import()` is keyed to the modules it actually loaded: an edit to one of them re-runs that test file's mutants, while an edit to any file outside them keeps their verdicts. When the runtime target is not a project file, such as a `data:` URL, the test file stays keyed to the whole project and its mutants re-run on any change. `ImportClosure.analyzeImportClosure` accepts `observedModules`: for each test file, the modules a dry run saw it load. Each one that names an existing file joins that test file's closure. A test file that reaches a computed dynamic `import()` then closes around the files it actually loaded, instead of falling back to the whole project. A key that names no existing file keeps the closure open. An incremental run now reuses `CompileError` verdicts instead of type-checking
168
+ every one again. A verdict is kept only while nothing that fed the check
169
+ changed: the mutant and its replacement, every file the checker's TypeScript
170
+ program loaded (including transitive and bundled declaration files), every
171
+ tsconfig in the `extends` chain and its project references, the TypeScript
172
+ version, the checker version and its options. Otherwise the mutant is
173
+ re-checked. The reuse stream line reports such refusals as `programChanged`
174
+ (or `closureAnalysisFailed` when the closure analysis failed); lines without a
175
+ count still decode.
176
+
177
+ Breaking: a configured checker must answer the new `digest` RPC, so upgrade
178
+ checker plugins together with this release. Effect moves to `4.0.0-rc.117`, together with the `@effect/*` packages these libraries use. Projects that install `effect` next to them need the same release.
179
+
180
+ - The TypeScript checker and test-runner workers now bundle their own runtime, so the only modules they load from your project are the TypeScript compiler and your test framework.
181
+ - The test-runner plugin supports the framework's fifth major release.
182
+ - The ignorer interface re-exports the `oxc-parser` 0.150 AST, in which `FormalParameterRest.decorators` is `Array<Decorator>`. An incremental mutation run now reuses the verdicts of module-level mutants when an edit cannot reach any test that exercises them, instead of running them again on every edit. An edit that could still reach such a mutant continues to invalidate its verdict, so no previously passing result is kept by mistake. An incremental run in a workspace now keeps the verdicts of mutants whose tests reach a linked sibling package. Stryker used to look up that package's own imports under the project root instead of next to the package, so an import the project does not install itself (a dependency only the sibling declares) went unresolved. Every test closure through that package then counted as open, and any edit anywhere re-ran its mutants. Bare imports now resolve the way Node resolves them, from the importing file's directory upwards. A dry-run-only run with incremental mode on publishes its dry-run coverage to the incremental file, keeping any verdicts already there, so a later run that reads the file through incrementalSources skips its own dry run. The run-inputs digest no longer covers dryRunOnly, so a preflight and the runs that reuse its coverage share one digest; existing caches are refused once with runInputsChanged. A shard plan now prices only the mutants a run will actually execute: mutants the incremental cache will reuse are planned at zero cost instead of their previous run time, so an unchanged project plans a single shard with zero predicted seconds. The plan still lists every mutant, so sharded runs keep reporting the full mutant set. `stryker plan` now reports, per project, how many mutants were reused, how many will run, the refused count by reason, and the reason any whole incremental report was discarded — on the machine `plan` stream line and as one human line per project. A plan that refuses reuse is no longer silent: the next warm run names the reason instead of only pricing a full run. Keep the plan's progress stream out of the projects it prices.
183
+
184
+ Planning a monorepo from its root walked into the first project and left
185
+ `reports/mutation-stream.jsonl` there, which made that project's file set differ
186
+ from the set its recorded verdicts were computed against. Running that project
187
+ afterwards then refused every verdict as `closureChanged` and mutated everything
188
+ again. A relative progress stream file is now resolved against the directory you
189
+ invoked the command from, so a plan's stream lands beside the plan output and a
190
+ following run of any project reuses its recorded verdicts. A verdict the engine decided without running a test now records the cost it measured. A mutant whose checker rejected it, or which no test covered while a checker looked at it, records its share of that check call's wall time; a mutant every configured checker ignored, or one left uncovered with no checker configured, records zero. A static mutant that a checker rejected carries that check time in the `StaticVerdict` event's `costMs` as well. A shard plan therefore balances on recorded times instead of pricing those verdicts at a whole-suite prediction, which previously dominated the plan. Killed, Survived and Timeout verdicts keep the measured test time they already recorded, and the run budget still counts only the verdicts that ran a test. A merged shard report now carries the cost record of every shard, so a later plan sees measured timings for all mutants. Previously only the first shard's costs survived the merge, and the mutants of every other shard fell back to their predicted cost whenever the next plan was derived. Enable the `@effect/language-service` tsgo plugin in every source package's `tsconfig.app.json` and `tsconfig.test.json`, and bump `@effect/tsgo` to `^0.50.0`. This turns on Effect-aware diagnostics during `effect-tsgo` type checking; it changes no runtime behaviour or public API. An incremental run now keeps a mutant's previous verdict when the files its covering tests can reach are unchanged, even though other files in the project changed. Editing an unrelated source file no longer re-runs verdicts it cannot affect: a verdict is discarded only when a file the mutant's covering tests can import — including sources reached through a workspace link — has itself changed. A setup file the runner reports from inside `node_modules` is hashed into the closures it feeds instead of making every verdict suspect, while installed packages stay covered by the lockfile and the package manifest.
191
+
3
192
  ## 17.0.2
4
193
 
5
194
  ### Patch Changes
package/README.md CHANGED
@@ -8,18 +8,13 @@ typed `./config` authoring surface.
8
8
  ## Install
9
9
 
10
10
  ```bash
11
- pnpm add -D @systemfsoftware/stryker-js
11
+ pnpm add -D @systemfsoftware/stryker-js vitest
12
+ node node_modules/@systemfsoftware/stryker-js/dist/main.mjs run
12
13
  ```
13
14
 
14
- ## Quick Start: Vitest Runner
15
-
16
- For TypeScript codebases using Vitest:
15
+ Add the runner, the TypeScript checker and the ignorers you use from npm as well, for example `pnpm add -D @systemfsoftware/stryker-js-vitest-runner @systemfsoftware/stryker-js-typescript-checker`. A bare plugin name in a config resolves from your project's dependencies. A plugin the package does not ship is named by its `file:` URL, for example `import.meta.resolve('my-ignorer')`.
17
16
 
18
- ```bash
19
- pnpm add -D @systemfsoftware/stryker-js \
20
- @systemfsoftware/stryker-js-vitest-runner \
21
- @systemfsoftware/stryker-js-typescript-checker
22
- ```
17
+ ## Quick Start: Vitest Runner
23
18
 
24
19
  Create `stryker.config.ts` in your project root:
25
20
 
@@ -49,7 +44,7 @@ export default defineConfig({
49
44
  Run mutation testing:
50
45
 
51
46
  ```bash
52
- pnpm exec stryker run
47
+ stryker run
53
48
  ```
54
49
 
55
50
  ## Zero-Plugin Built-in Runners
@@ -139,45 +134,52 @@ export default mergeConfig(baseConfig, {
139
134
 
140
135
  ```bash
141
136
  # Run mutation testing
142
- pnpm exec stryker run [options]
137
+ stryker run [options]
143
138
 
144
139
  # Mutate specific files only
145
- pnpm exec stryker run --mutate "src/auth/*.ts"
140
+ stryker run --mutate "src/auth/*.ts"
146
141
 
147
142
  # Scope the run to lines changed since a git ref
148
- pnpm exec stryker run --since origin/main
143
+ stryker run --since origin/main
149
144
 
150
145
  # Re-run named mutants from the previous report
151
- pnpm exec stryker run --mutant <id,...>
146
+ stryker run --mutant <id,...>
152
147
 
153
148
  # Re-test only surviving mutants from prior run
154
- pnpm exec stryker run --survivors
149
+ stryker run --survivors
155
150
 
156
151
  # Incremental caching is on by default; re-verify every mutant instead
157
- pnpm exec stryker run --full
152
+ stryker run --full
158
153
 
159
154
  # Run with specific concurrency
160
- pnpm exec stryker run --concurrency 4
155
+ stryker run --concurrency 4
161
156
 
162
157
  # Merge partial mutation reports from parallel CI shards
163
- pnpm exec stryker merge --plan plan.json reports/shards/1 reports/shards/2 --out reports/mutation
158
+ stryker merge --plan plan.json reports/shards/1 reports/shards/2 --out reports/mutation
164
159
 
165
160
  # Fail on survivors absent from a committed baseline
166
- pnpm exec stryker gate --baseline .stryker-baseline.json
161
+ stryker gate --baseline .stryker-baseline.json
167
162
 
168
163
  # Name every mutant whose status differs between two reports
169
- pnpm exec stryker compare --baseline reports/mutation-report.json --fresh reports/fresh.json
164
+ stryker compare --baseline reports/mutation-report.json --fresh reports/fresh.json
170
165
 
171
166
  # Annotate a pull request at each surfaced survivor
172
- pnpm exec stryker annotate --baseline .stryker-baseline.json
167
+ stryker annotate --baseline .stryker-baseline.json
168
+
169
+ # Check every dropped mutant against a kill matrix, or count a finished run
170
+ stryker audit --matrix kill-matrix/ --projects packages/a,packages/b --out reports/audit.json
171
+ stryker audit --matrix kill-matrix/ --projects packages/a --files packages/a/src/sum.ts --out reports/audit.json
172
+ stryker audit --counts-only --matrix mutation-report/ --out reports/counts.json
173
173
 
174
174
  # Judge one surfaced survivor, or drive runs from an editor or an agent
175
- pnpm exec stryker feedback <id> --not-useful --reason "logging only"
176
- pnpm exec stryker serve stdio
177
- pnpm exec stryker mcp
175
+ stryker feedback <id> --not-useful --reason "logging only"
176
+ stryker serve stdio
177
+ stryker mcp
178
178
  ```
179
179
 
180
- `stryker run --since <ref>` takes the merge base of `<ref>` and the working tree, diffs it at `--unified=0`, and intersects the changed lines with `mutate`; it falls back to a full run when the Stryker config, a test-runner config, `package.json`, or the lockfile changed. `stryker run --mutant <id,...>` admits each id against the previous report (`reports/mutation/mutation.json`), restricts the run to those mutants' files, and emits each one's status, covering tests, killing test, and a `stryker run --mutant <id>` reproducer. `stryker gate` exits non-zero on survivors missing from its baseline, `stryker compare` exits `1` on a status mismatch after subtracting a `--noise` file, and `stryker annotate` prints GitHub workflow commands at each surfaced survivor.
180
+ `stryker run --since <ref>` takes the merge base of `<ref>` and the working tree, diffs it at `--unified=0`, and intersects the changed lines with `mutate`; it falls back to a full run when the Stryker config, a test-runner config, `package.json`, or the lockfile changed in the project or in one of its ancestor directories. The same files in another directory, such as a sibling workspace package, keep the diff scope. `stryker run --mutant <id,...>` admits each id against the previous report (`reports/mutation/mutation.json`), restricts the run to those mutants' files, and emits each one's status, covering tests, killing test, and a `stryker run --mutant <id>` reproducer. `stryker gate` exits non-zero on survivors missing from its baseline, and exits `1` with `score-below-break` when a project's mutation score falls below that project's own `thresholds.break` (`stryker merge` records each project's thresholds in the merged report, so a sharded run is judged like an unsharded one). `stryker compare` exits `1` on a status mismatch after subtracting a `--noise` file, and `stryker annotate` prints GitHub workflow commands at each surfaced survivor.
181
+
182
+ `stryker audit` checks that the default mutant-set policy loses no kill. `--matrix` is a directory holding `<project>/stryker-incremental.json` for each `--projects` entry, recorded by a run with every killer kept (the shared config's `STRYKER_KILL_MATRIX=1` lane); a report written under any policy but `full` is refused, because an ordinary run never runs the mutants the default policy drops. The audit instruments the matrix sources without running a test, joins each dropped mutant `m` to its dominator `d` by id, and judges the pair: `Pass` when every named test that kills `d` also kills `m` (`killers-contained`) or `m` was never compiled (`dropped-mutant-carries-no-kill-signal`); `Vacuous` when `d` survived or had no coverage; `AttributionUnverified` when a killer is a file-level hook, the matrix names no test for it, or either mutant timed out; `Fail` when a named test kills `d` but not `m`, `d` never ran, `m` survived, or the ids do not match. A test whose every killed mutant is dropped is reported as orphaned. `--files` scopes the audit to the drops in the named files, each written as `<--projects entry>/<path in that project>`, so a pull request audits only what it changes; a named file the matrix does not record is refused. The audit exits `0` when no pair fails, no rule goes unattested and no test is orphaned, `1` otherwise, and `2` on unusable input (an unreadable, undecodable or not-`full` matrix, a `--files` entry outside the matrix, reports that record no mutant, or `--files` with `--counts-only`). `--counts-only` reads the reports alone and counts planned, `CompileError`, compiled and executed mutants and test executions per project, with ignored mutants grouped by rule. Both modes write a versioned JSON report (`schemaVersion`) to `--out`, where every pair, unattested rule and orphaned test carries a reason code and a next action, and print a summary capped at 20 detail lines.
181
183
 
182
184
  A mutation run only starts on main CI. `stryker run` — and the programmatic `run`/`strykerCell` entry, the MSP `mutationTest` request, and the MCP `rerun_mutant` tool — refuses to start unless `GITHUB_ACTIONS` is `true`. A refused run exits non-zero and ends the machine stream with a `refused` event naming the rule (`mutation-runs-on-main-ci`). Dry runs (`--dryRunOnly`), `--version`, config and help commands, the MSP `discover` request, and the read-only MCP tools are unaffected. `stryker serve stdio|socket` speaks the Mutation Server Protocol and `stryker mcp` serves MCP over stdio.
183
185
 
@@ -186,19 +188,19 @@ A mutation run only starts on main CI. `stryker run` — and the programmatic `r
186
188
  Human output is the default, whether or not `stdout` is a terminal. A machine consumer opts into the NDJSON event stream explicitly:
187
189
 
188
190
  ```bash
189
- pnpm exec stryker run --json # wire records on stdout, diagnostics on stderr
190
- STRYKER_MODE=machine pnpm exec stryker run # the same, named by environment
191
+ stryker run --json # wire records on stdout, diagnostics on stderr
192
+ STRYKER_MODE=machine stryker run # the same, named by environment
191
193
  ```
192
194
 
193
- `--format text` names the human format explicitly; `--json` together with `--format text` is a usage error (exit 2). Under `--json`, `stdout` carries wire records and nothing else — progress lines and log output stay on `stderr`. Every run also writes the same records to `reports/mutation-stream.jsonl` (`--progressStreamFile`) in both modes, which is the artifact `stryker merge` rebuilds a shard's partial report from.
195
+ `--format text` names the human format explicitly; `--json` together with `--format text` is a usage error (exit 2). Under `--json`, `stdout` carries wire records and nothing else — progress lines and log output stay on `stderr`. Every run also writes the same records to the `progressStreamFile` (`--progressStreamFile`, or for `stryker run` also the config file, default `reports/mutation-stream.jsonl`) in both modes, which is the artifact `stryker merge` rebuilds a shard's partial report from. A run never counts its own stream file, at the path it resolved or at the default path, as part of the project, so re-running with the same settings never re-runs a mutant because of it; the path is matched as an ignore pattern, with exact case where the file system tells case apart, so keep glob characters (`!`, `*`, `?`, `[`, `{`) out of it. A stream path naming an existing file whose first line is neither a Stryker run event record, a `stream` header of any schema version (a `stream` object carrying string `schemaVersion` and `runId`) nor, as the whole file, an unterminated line that opens like one and reads as the unfinished start of a JSON value (what a run that died mid-write leaves) is never overwritten. Only the first 1 MiB is read, so a first line still unterminated after 1 MiB is no record. That case, and a stream file that cannot be opened or written, is logged as `stryker.output.drain_file_failed`, and the `--json` records still reach `stdout`.
194
196
 
195
197
  ## Reporters, Sidecars, and Verdict Reuse
196
198
 
197
199
  `reporters` defaults to `['clear-text', 'progress', 'html']`. Add `sarif` to also write `reports/mutation/mutation.sarif` (SARIF 2.1.0, named from `jsonReporter.fileName`): each survivor is a `warning` result, each no-coverage mutant a `note`, the fingerprint is the mutant's content id, and the log is capped at 5,000 results. Every run writes `reports/mutation/reproducers.json`: one entry per mutant in the report, holding its mutated-lines diff and the `stryker run --mutant <id>` command that reproduces it.
198
200
 
199
- Incremental reuse is on by default: an unchanged mutant whose covering tests are unchanged is re-used and the verdict records its `incrementalMode` (`incremental` or `full`). Pass `--full` to re-verify every mutant, ignoring the cache and the persisted dry run. The incremental cache is keyed by content: a mutant's id, the import-closure digest of its covering tests, the run inputs, the verdict-semantics version, and the mutant-set policy. Nothing in the key names a shard, branch, report path, or machine, so verdicts from different runs union and are reused wherever their inputs match. `incrementalSources` accepts globs of further incremental reports to union beside `incrementalFile`, which is how a sharded CI workspace reuses the reports it restored from other shards. Each run's stream carries a `reuse` line with the reused, ran, and per-reason refused counts, and an unchanged project reuses its persisted initial test run instead of repeating it.
201
+ Incremental reuse is on by default: an unchanged mutant whose covering tests are unchanged is re-used and the verdict records its `incrementalMode` (`incremental` or `full`). Pass `--full` to re-verify every mutant, ignoring the cache and the persisted dry run. The incremental cache is keyed by content: a mutant's id, the import-closure digest of its covering tests, the run inputs, a digest of the engine's installed files, and the mutant-set policy; a verdict written by another build of the engine is refused as `semanticsChanged`. Nothing in the key names a shard, branch, report path, or machine, so verdicts from different runs union and are reused wherever their inputs match. `incrementalSources` accepts globs of further incremental reports to union beside `incrementalFile`, which is how a sharded CI workspace reuses the reports it restored from other shards. Each run's stream carries a `reuse` line with the reused, ran, and per-reason refused counts, and an unchanged project reuses its persisted initial test run instead of repeating it.
200
202
 
201
- `mutator.mutantSetPolicy` defaults to `'default'`, which suppresses the mutants a rule proves redundant — a relational replacement outside the sufficient set, a conditional that collapses to a literal, a replacement equal to the original code, or a duplicate already planted at the site — and records the rule id in the mutant's report entry. Set `mutator: { mutantSetPolicy: 'full' }` to keep every variant. `surfacing` (`{ perLine: 1, perFile: 7 }`) caps how many survivors reach the review surfaces and SARIF, without changing what the engine computes.
203
+ `mutator.mutantSetPolicy` defaults to `'default'`, which suppresses the mutants a rule proves redundant — a replacement equal to the original code, a duplicate already planted at the site, or the complement of a kept ordering mutant (`redundant-relational`: for `a < b`, `a >= b` is dropped beside `a <= b`, because every test that kills `a <= b` also kills it) — and records the rule id in the mutant's report entry. A dropped complement carries a `subsumption` reference naming its dominator; when no dominator runs (the checker ignores it, it fails to compile, or it is remembered without running) the complement runs after all and its reference becomes `Readmitted`, naming each cause. Set `mutator: { mutantSetPolicy: 'full' }` to keep every variant. `surfacing` (`{ perLine: 1, perFile: 7 }`) caps how many survivors reach the review surfaces and SARIF, without changing what the engine computes.
202
204
 
203
205
  ### Upgrading from 13.x: the mutant set changed in 14.0.0
204
206
 
@@ -244,6 +246,12 @@ console.log(`Mutation score: ${verdict.score}%`)
244
246
  | `./config` | Config authoring surface (`defineConfig`, `mergeConfig`, `StrykerConfig`) |
245
247
  | `./promises` | `run()` wrapper returning standard JavaScript promises |
246
248
 
249
+ ## Known issues
250
+
251
+ On Node 24.x (and 26.x up to at least 26.7.0), V8's `JSON.parse` can return the wrong key in a long-lived process. After the process has parsed an object key ending in an escaped backslash, a later key at the same key position that ends in an escape can come back substituted. This affects reading Stryker's report JSON in the same process. See [nodejs/node#63785](https://github.com/nodejs/node/issues/63785) and the unmerged backport [nodejs/node#66542](https://github.com/nodejs/node/pull/66542).
252
+
253
+ Re-check and remove this entry once a Node 24 release carries the #66542 backport.
254
+
247
255
  ## License
248
256
 
249
257
  [Apache-2.0](../../LICENSE)
@@ -1,4 +1,4 @@
1
- import { a as mod_d_exports$6 } from "./index-y-Pg3XgT.mjs";
1
+ import { a as mod_d_exports$6 } from "./index-ClWSuUog.mjs";
2
2
  import * as S from "effect/Schema";
3
3
  //#region src/Run.schema.d.ts
4
4
  declare const TypeId: '~stryker/mutation-run/StageError';
@@ -7,7 +7,9 @@ declare const StageError_base: S.Class<StageError, S.TaggedStruct<"StageError",
7
7
  readonly reason: S.String;
8
8
  readonly cause: S.optional<S.Defect>;
9
9
  readonly command: S.optional<S.String>;
10
- }>, import("effect/Cause").YieldableError>;
10
+ }>, import("effect/Cause").YieldableError & {
11
+ readonly brand: unique symbol;
12
+ }>;
11
13
  declare class StageError extends StageError_base {
12
14
  readonly [TypeId]: "~stryker/mutation-run/StageError";
13
15
  get exitClass(): mod_d_exports$6.ExitClass;
@@ -16,7 +18,9 @@ declare class StageError extends StageError_base {
16
18
  declare const PrepareError_base: S.Class<PrepareError, S.TaggedStruct<"PrepareError", {
17
19
  readonly stage: S.Literal<"prepare">;
18
20
  readonly reason: S.String;
19
- }>, import("effect/Cause").YieldableError>;
21
+ }>, import("effect/Cause").YieldableError & {
22
+ readonly brand: unique symbol;
23
+ }>;
20
24
  declare class PrepareError extends PrepareError_base {
21
25
  get message(): string;
22
26
  }