@systemfsoftware/stryker-js-cli 4.0.3 → 5.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,27 @@
1
1
  # @systemfsoftware/stryker-js-cli
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - The Node host package `@systemfsoftware/stryker-js-platform-node` is gone: the engine (`@systemfsoftware/stryker-js-engine`) is the host-neutral mutation run, and the `stryker` CLI owns the Node composition roots and worker entries. Depend on the engine for the run and the CLI for process entries; install the new names.
8
+
9
+ - RunOutcome now places the genuine verdicts (RunParseFailed, RunSurvivorsRejected, RunConfigFailed, RunFailed) on the decision channel as a branded tagged union; RunInterrupted remains a failure. Output.workflow resolves a branded HumanOutput|MachineOutput union instead of a plain record
10
+
11
+ - Disposing a worker now terminates the whole process group and escalates. The
12
+ runner signals the group so a worker's own child processes go with it, waits for
13
+ the exit, and sends `SIGKILL` if the worker is still alive two seconds later.
14
+ Previously a single `SIGTERM` went to one process id and the runner moved on
15
+ after a fixed wait, so a worker that installed a `SIGTERM` handler — or that had
16
+ spawned children of its own — outlived the run.
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies:
21
+ - @systemfsoftware/effect-cell-types@6.0.0
22
+ - @systemfsoftware/stryker-js@1.0.0
23
+ - @systemfsoftware/stryker-js-html-reporter@1.0.0
24
+
3
25
  ## 4.0.3
4
26
 
5
27
  ### Patch Changes
package/README.md CHANGED
@@ -84,7 +84,7 @@ The highest pending class wins; a terminating signal outranks all of them.
84
84
 
85
85
  ## Related
86
86
 
87
- The mutation engine is [`@systemfsoftware/stryker-js-platform-node`][engine].
87
+ The mutation engine is [`@systemfsoftware/stryker-js-engine`][engine].
88
88
  This package is the terminal-facing half and reaches it through an injected
89
89
  run-event sink;
90
90
  it ships a command and exposes no importable API.
@@ -100,5 +100,5 @@ Licensed under [Apache 2.0][license-url].
100
100
  [license-badge]: https://img.shields.io/badge/license-Apache_2.0-blue?style=flat-square
101
101
  [license-url]: https://github.com/systemfsoftware/systemfsoftware/blob/main/LICENSE
102
102
  [repo]: https://github.com/systemfsoftware/systemfsoftware
103
- [engine]: https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/testing/mutation/stryker-js/platform-node
103
+ [engine]: https://github.com/systemfsoftware/systemfsoftware/tree/main/packages/testing/mutation/stryker-js/engine
104
104
  [docs]: https://stryker-mutator.io/docs/stryker-js/configuration/
package/dist/main.mjs CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import * as NodeFileSystem$1 from "@effect/platform-node-shared/NodeFileSystem";
3
- import * as NodePath$1 from "@effect/platform-node-shared/NodePath";
2
+ import { n as nodePlatformLayer } from "./node-D-ynY_kk.mjs";
3
+ import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
4
+ import * as NodePath from "@effect/platform-node-shared/NodePath";
4
5
  import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
5
6
  import * as NodeStdio from "@effect/platform-node/NodeStdio";
6
- import { ConfigFileUnreadableError, buildVerdictEnvelope, defaultOptions, generateRunId, makeRunLayer, nodeModuleLayer, readConfig, runMutationTest, strykerEngines, strykerVersion, toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-platform-node";
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Exit from "effect/Exit";
9
9
  import * as Layer from "effect/Layer";
10
10
  import * as Logger from "effect/Logger";
11
11
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
12
+ import { ConfigFileUnreadableError, buildVerdictEnvelope, defaultOptions, generateRunId, makeRunLayer, readConfig, runMutationTest, strykerVersion, toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-engine";
13
+ import { Mutant, causeText } from "@systemfsoftware/stryker-js/Mutant";
12
14
  import { Heartbeat, HelpRendered, ManifestRendered, RunEvents, RunFailed, RunStarted, VerdictReached } from "@systemfsoftware/stryker-js/Run";
13
15
  import { RENDERED_OPTION_DEFAULTS } from "@systemfsoftware/stryker-js/Schema";
14
16
  import * as Cause from "effect/Cause";
@@ -29,94 +31,21 @@ import * as CliError from "effect/unstable/cli/CliError";
29
31
  import * as Command from "effect/unstable/cli/Command";
30
32
  import * as Flag from "effect/unstable/cli/Flag";
31
33
  import * as GlobalFlag from "effect/unstable/cli/GlobalFlag";
34
+ import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
32
35
  import { ExitClass, highestExitClass } from "@systemfsoftware/stryker-js/ExitClass";
33
- import { Mutant, causeText } from "@systemfsoftware/stryker-js/Mutant";
34
36
  import * as Clock from "effect/Clock";
35
37
  import * as Formatter from "effect/Formatter";
36
38
  import * as Predicate from "effect/Predicate";
37
- import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
38
39
  import * as Context from "effect/Context";
39
- import { pipe } from "effect/Function";
40
40
  import * as Stdio from "effect/Stdio";
41
41
  import * as Stream from "effect/Stream";
42
- import { NodeFileSystem, NodePath } from "@effect/platform-node";
42
+ import "@systemfsoftware/stryker-js/Module";
43
43
  import { sha256 } from "@noble/hashes/sha256";
44
44
  import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
45
- import "@systemfsoftware/stryker-js/Module";
46
- //#region src/RunOutcome.workflow.ts
47
- var RunOutcomeCommand = class extends S.TaggedClass()("RunOutcomeCommand", {
48
- succeeded: S.Boolean,
49
- signal: S.optional(S.Finite),
50
- interrupted: S.Boolean,
51
- helpErrorCount: S.optional(S.Finite),
52
- cliError: S.Boolean,
53
- unrecognized: S.optional(S.String),
54
- survivorsReason: S.optional(S.Literals(["no-report", "mismatch"])),
55
- survivorsDiagnostic: S.optional(S.String),
56
- schemaError: S.Boolean,
57
- successExitClass: S.optional(ExitClass),
58
- highestExitClass: S.optional(ExitClass),
59
- configDetail: S.optional(S.String),
60
- diagnostic: S.optional(S.String)
61
- }) {};
62
- const CONFIG_CODE$1 = 2;
63
- const classCode = (exitClass) => Match.value(exitClass).pipe(Match.when("VerdictFail", () => 1), Match.when("ConfigError", () => CONFIG_CODE$1), Match.when("RuntimeError", () => 3), Match.when("InternalError", () => 4), Match.exhaustive);
64
- var RunOk = class extends S.TaggedClass()("RunOk", { help: S.Boolean }) {};
65
- var RunInterrupted = class extends S.TaggedError()("RunInterrupted", { code: S.Finite }) {};
66
- var RunParseFailed = class extends S.TaggedError()("RunParseFailed", { unrecognized: S.optional(S.String) }) {};
67
- var RunSurvivorsRejected = class extends S.TaggedError()("RunSurvivorsRejected", {
68
- reason: S.Literals(["no-report", "mismatch"]),
69
- diagnostic: S.optional(S.String)
70
- }) {};
71
- var RunConfigFailed = class extends S.TaggedError()("RunConfigFailed", { detail: S.optional(S.String) }) {};
72
- var RunFailed$1 = class extends S.TaggedError()("RunFailed", {
73
- code: S.Finite,
74
- diagnostic: S.optional(S.String)
75
- }) {};
76
- function classify(command) {
77
- if (command.signal !== void 0) return RunInterrupted.make({ code: 128 + command.signal });
78
- if (command.succeeded) {
79
- if (command.successExitClass !== void 0) return RunFailed$1.make({
80
- code: classCode(command.successExitClass),
81
- diagnostic: command.diagnostic
82
- });
83
- return RunOk.make({ help: false });
84
- }
85
- if (command.interrupted) return RunInterrupted.make({ code: 1 });
86
- if (command.helpErrorCount !== void 0) {
87
- if (command.helpErrorCount > 0) return RunParseFailed.make({ unrecognized: command.unrecognized });
88
- return RunOk.make({ help: true });
89
- }
90
- if (command.cliError) return RunParseFailed.make({ unrecognized: command.unrecognized });
91
- if (command.survivorsReason !== void 0) return RunSurvivorsRejected.make({
92
- reason: command.survivorsReason,
93
- diagnostic: command.survivorsDiagnostic
94
- });
95
- if (command.schemaError) return RunConfigFailed.make({ detail: command.configDetail });
96
- if (command.highestExitClass !== void 0) {
97
- if (command.highestExitClass === "ConfigError") return RunConfigFailed.make({ detail: command.configDetail });
98
- return RunFailed$1.make({
99
- code: classCode(command.highestExitClass),
100
- diagnostic: command.diagnostic
101
- });
102
- }
103
- return RunFailed$1.make({
104
- code: 1,
105
- diagnostic: command.diagnostic
106
- });
107
- }
108
- function succeedRun(ok) {
109
- return Result.succeed(ok);
110
- }
111
- function failRun(error) {
112
- return Result.fail(error);
113
- }
114
- function runOutcomeDecision(command) {
115
- return Match.value(classify(command)).pipe(Match.tag("RunOk", succeedRun), Match.tag("RunInterrupted", failRun), Match.tag("RunParseFailed", failRun), Match.tag("RunSurvivorsRejected", failRun), Match.tag("RunConfigFailed", failRun), Match.tag("RunFailed", failRun), Match.exhaustive);
116
- }
117
- const runOutcomeWorkflow = Workflow.make(RunOutcomeCommand, runOutcomeDecision);
45
+ //#region package.json
46
+ var engines = { "node": ">=20.0.0" };
118
47
  //#endregion
119
- //#region src/Survivors.workflow.ts
48
+ //#region src/admit-survivors-run.workflow.ts
120
49
  /**
121
50
  * The mutant shape the admission carries, named once because both the decision's
122
51
  * `Admitted` payload and the command's precomputed survivor list are the same shape.
@@ -316,20 +245,91 @@ function admissionDecision(command) {
316
245
  }
317
246
  const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, admissionDecision);
318
247
  //#endregion
248
+ //#region src/classify-run-outcome.workflow.ts
249
+ var RunOutcomeCommand = class extends S.TaggedClass()("RunOutcomeCommand", {
250
+ succeeded: S.Boolean,
251
+ signal: S.optional(S.Finite),
252
+ interrupted: S.Boolean,
253
+ helpErrorCount: S.optional(S.Finite),
254
+ cliError: S.Boolean,
255
+ unrecognized: S.optional(S.String),
256
+ survivorsReason: S.optional(S.Literals(["no-report", "mismatch"])),
257
+ survivorsDiagnostic: S.optional(S.String),
258
+ schemaError: S.Boolean,
259
+ successExitClass: S.optional(ExitClass),
260
+ highestExitClass: S.optional(ExitClass),
261
+ configDetail: S.optional(S.String),
262
+ diagnostic: S.optional(S.String)
263
+ }) {};
264
+ const CONFIG_CODE$1 = 2;
265
+ const classCode = (exitClass) => Match.value(exitClass).pipe(Match.when("VerdictFail", () => 1), Match.when("ConfigError", () => CONFIG_CODE$1), Match.when("RuntimeError", () => 3), Match.when("InternalError", () => 4), Match.exhaustive);
266
+ const RunOutcomeTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/RunOutcome");
267
+ var RunOk = class extends S.TaggedClass()("RunOk", { help: S.Boolean }) {
268
+ [RunOutcomeTypeId] = RunOutcomeTypeId;
269
+ };
270
+ var RunInterrupted = class extends S.TaggedError()("RunInterrupted", { code: S.Finite }) {
271
+ [RunOutcomeTypeId] = RunOutcomeTypeId;
272
+ };
273
+ var RunParseFailed = class extends S.TaggedClass()("RunParseFailed", { unrecognized: S.optional(S.String) }) {
274
+ [RunOutcomeTypeId] = RunOutcomeTypeId;
275
+ };
276
+ var RunSurvivorsRejected = class extends S.TaggedClass()("RunSurvivorsRejected", {
277
+ reason: S.Literals(["no-report", "mismatch"]),
278
+ diagnostic: S.optional(S.String)
279
+ }) {
280
+ [RunOutcomeTypeId] = RunOutcomeTypeId;
281
+ };
282
+ var RunConfigFailed = class extends S.TaggedClass()("RunConfigFailed", { detail: S.optional(S.String) }) {
283
+ [RunOutcomeTypeId] = RunOutcomeTypeId;
284
+ };
285
+ var RunFailed$1 = class extends S.TaggedClass()("RunFailed", {
286
+ code: S.Finite,
287
+ diagnostic: S.optional(S.String)
288
+ }) {
289
+ [RunOutcomeTypeId] = RunOutcomeTypeId;
290
+ };
291
+ function classify(command) {
292
+ if (command.signal !== void 0) return RunInterrupted.make({ code: 128 + command.signal });
293
+ if (command.succeeded) {
294
+ if (command.successExitClass !== void 0) return RunFailed$1.make({
295
+ code: classCode(command.successExitClass),
296
+ diagnostic: command.diagnostic
297
+ });
298
+ return RunOk.make({ help: false });
299
+ }
300
+ if (command.interrupted) return RunInterrupted.make({ code: 1 });
301
+ if (command.helpErrorCount !== void 0) {
302
+ if (command.helpErrorCount > 0) return RunParseFailed.make({ unrecognized: command.unrecognized });
303
+ return RunOk.make({ help: true });
304
+ }
305
+ if (command.cliError) return RunParseFailed.make({ unrecognized: command.unrecognized });
306
+ if (command.survivorsReason !== void 0) return RunSurvivorsRejected.make({
307
+ reason: command.survivorsReason,
308
+ diagnostic: command.survivorsDiagnostic
309
+ });
310
+ if (command.schemaError) return RunConfigFailed.make({ detail: command.configDetail });
311
+ if (command.highestExitClass !== void 0) {
312
+ if (command.highestExitClass === "ConfigError") return RunConfigFailed.make({ detail: command.configDetail });
313
+ return RunFailed$1.make({
314
+ code: classCode(command.highestExitClass),
315
+ diagnostic: command.diagnostic
316
+ });
317
+ }
318
+ return RunFailed$1.make({
319
+ code: 1,
320
+ diagnostic: command.diagnostic
321
+ });
322
+ }
323
+ const classifyRunOutcome$1 = Workflow.make(RunOutcomeCommand, (command) => Match.value(classify(command)).pipe(Match.tag("RunInterrupted", (error) => Result.fail(error)), Match.when((outcome) => !(outcome instanceof RunInterrupted), (decision) => Result.succeed(decision)), Match.exhaustive));
324
+ //#endregion
319
325
  //#region src/Envelope.ts
320
- /**
321
- * Envelope — the failure envelope and console capture leaf.
322
- *
323
- * Extracted from Cli.ts to break the import cycle Cli <-> Output.
324
- * Both Cli and Output import from this leaf, so neither depends on the other
325
- * for these values. This file imports only from external packages and from
326
- * StreamVersion (leaf) and Survivors.workflow (leaf), never from Cli or Output
327
- * themselves.
328
- */
329
326
  const CONFIG_CODE = 2;
330
327
  function runOutcomeCode(result) {
331
- if (Result.isSuccess(result)) return 0;
332
- return Match.value(result.failure).pipe(Match.tag("RunInterrupted", (error) => error.code), Match.tag("RunParseFailed", () => CONFIG_CODE), Match.tag("RunSurvivorsRejected", () => CONFIG_CODE), Match.tag("RunConfigFailed", () => CONFIG_CODE), Match.tag("RunFailed", (error) => error.code), Match.exhaustive);
328
+ const outcome = Result.match(result, {
329
+ onSuccess: (success) => success,
330
+ onFailure: (failure) => failure
331
+ });
332
+ return Match.value(outcome).pipe(Match.tag("RunOk", () => 0), Match.tag("RunInterrupted", (error) => error.code), Match.tag("RunParseFailed", () => CONFIG_CODE), Match.tag("RunSurvivorsRejected", () => CONFIG_CODE), Match.tag("RunConfigFailed", () => CONFIG_CODE), Match.tag("RunFailed", (failed) => failed.code), Match.exhaustive);
333
333
  }
334
334
  function isExitClass(value) {
335
335
  return S.is(ExitClass)(value);
@@ -576,7 +576,7 @@ function shapeEnvelope(error, captured) {
576
576
  };
577
577
  }
578
578
  function classifyRunOutcome(exit, signal, argv) {
579
- return runOutcomeWorkflow(gatherRunOutcome(exit, signal, argv));
579
+ return classifyRunOutcome$1(gatherRunOutcome(exit, signal, argv));
580
580
  }
581
581
  const capturedConsoleChunks = [];
582
582
  const countByLabel = /* @__PURE__ */ new Map();
@@ -683,12 +683,8 @@ function resetCapturedConsole() {
683
683
  timeByLabel.clear();
684
684
  }
685
685
  //#endregion
686
- //#region src/Output.workflow.ts
686
+ //#region src/resolve-output-mode.workflow.ts
687
687
  const TOOL_VARIABLES$1 = ["CLAUDECODE", "CODEX_SANDBOX"];
688
- /**
689
- * The command of the output-mode workflow: a schema class, because `Workflow.make`
690
- * derives the command type from it and pins the error channel at the construction site.
691
- */
692
688
  var ResolveModeCommand = class extends S.TaggedClass()("ResolveModeCommand", {
693
689
  stdoutIsTTY: S.Boolean,
694
690
  text: S.optional(S.Boolean),
@@ -697,17 +693,33 @@ var ResolveModeCommand = class extends S.TaggedClass()("ResolveModeCommand", {
697
693
  agent: S.optional(S.String),
698
694
  toolVars: S.optional(S.Record(S.String, S.String))
699
695
  }) {};
700
- /**
701
- * The conflict error for mutually exclusive format flags. Defined locally so the
702
- * workflow decision remains pure — the sealed effect surface does not include
703
- * `effect/unstable/cli/CliError`, so the decision returns this local error and
704
- * the shell maps it to `CliError.InvalidValue` at the boundary.
705
- */
696
+ const ResolveModeTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/ResolveMode");
706
697
  var ModeConflictError = class extends S.TaggedError()("ModeConflictError", {
707
698
  option: S.String,
708
699
  value: S.String,
709
700
  expected: S.String
710
- }) {};
701
+ }) {
702
+ [ResolveModeTypeId] = ResolveModeTypeId;
703
+ };
704
+ const ModeSignal = S.Literals([
705
+ "flag",
706
+ "env",
707
+ "tty",
708
+ "agent",
709
+ "tool"
710
+ ]);
711
+ var HumanOutput = class extends S.TaggedClass()("HumanOutput", {
712
+ signal: ModeSignal,
713
+ stdoutIsTTY: S.Boolean
714
+ }) {
715
+ [ResolveModeTypeId] = ResolveModeTypeId;
716
+ };
717
+ var MachineOutput = class extends S.TaggedClass()("MachineOutput", {
718
+ signal: ModeSignal,
719
+ stdoutIsTTY: S.Boolean
720
+ }) {
721
+ [ResolveModeTypeId] = ResolveModeTypeId;
722
+ };
711
723
  const CONFLICT_EXPECTED = "the \"--format text\" and \"--json\" flags are mutually exclusive — use one or the other";
712
724
  function r4(command) {
713
725
  if (command.text === true && command.json === true) return Result.fail(ModeConflictError.make({
@@ -715,71 +727,51 @@ function r4(command) {
715
727
  value: "text",
716
728
  expected: CONFLICT_EXPECTED
717
729
  }));
718
- if (command.text === true) return Result.succeed({
719
- mode: "human",
730
+ if (command.text === true) return Result.succeed(HumanOutput.make({
720
731
  signal: "flag",
721
732
  stdoutIsTTY: command.stdoutIsTTY
722
- });
723
- if (command.json === true) return Result.succeed({
724
- mode: "machine",
733
+ }));
734
+ if (command.json === true) return Result.succeed(MachineOutput.make({
725
735
  signal: "flag",
726
736
  stdoutIsTTY: command.stdoutIsTTY
727
- });
737
+ }));
728
738
  if (command.envMode !== void 0 && command.envMode.length > 0) {
729
- if (command.envMode === "machine") return Result.succeed({
730
- mode: "machine",
739
+ if (command.envMode === "machine") return Result.succeed(MachineOutput.make({
731
740
  signal: "env",
732
741
  stdoutIsTTY: command.stdoutIsTTY
733
- });
734
- return Result.succeed({
735
- mode: "human",
742
+ }));
743
+ return Result.succeed(HumanOutput.make({
736
744
  signal: "env",
737
745
  stdoutIsTTY: command.stdoutIsTTY
738
- });
746
+ }));
739
747
  }
740
- if (!command.stdoutIsTTY) return Result.succeed({
741
- mode: "machine",
748
+ if (!command.stdoutIsTTY) return Result.succeed(MachineOutput.make({
742
749
  signal: "tty",
743
750
  stdoutIsTTY: false
744
- });
745
- if (command.agent !== void 0 && command.agent.length > 0) return Result.succeed({
746
- mode: "machine",
751
+ }));
752
+ if (command.agent !== void 0 && command.agent.length > 0) return Result.succeed(MachineOutput.make({
747
753
  signal: "agent",
748
754
  stdoutIsTTY: true
749
- });
755
+ }));
750
756
  const toolVars = command.toolVars ?? {};
751
757
  for (const variable of TOOL_VARIABLES$1) {
752
758
  const value = toolVars[variable];
753
- if (typeof value === "string" && value.length > 0) return Result.succeed({
754
- mode: "machine",
759
+ if (typeof value === "string" && value.length > 0) return Result.succeed(MachineOutput.make({
755
760
  signal: "tool",
756
761
  stdoutIsTTY: true
757
- });
762
+ }));
758
763
  }
759
- return Result.succeed({
760
- mode: "human",
764
+ return Result.succeed(HumanOutput.make({
761
765
  signal: "tty",
762
766
  stdoutIsTTY: true
763
- });
767
+ }));
764
768
  }
765
769
  function modeDecision(command) {
766
770
  return r4(command);
767
771
  }
768
- const resolveModeWorkflow = Workflow.make(ResolveModeCommand, modeDecision);
772
+ const resolveOutputMode = Workflow.make(ResolveModeCommand, modeDecision);
769
773
  //#endregion
770
774
  //#region src/Output.ts
771
- /**
772
- * Output — the machine/human output capability.
773
- *
774
- * The NDJSON run-event stream, wire protocol constants, mode resolution probes,
775
- * and machine-mode terminal output. Pure mode resolution lives in
776
- * Output.workflow.ts.
777
- */
778
- /**
779
- * The heartbeat interval (R19), matching Terraform's `apply_progress`
780
- * cadence: long enough that a slow phase is not noisy, short enough that a
781
- * consumer can tell "slow" from "hung" without waiting for a mutant event.
782
- */
783
775
  const TICK_INTERVAL_MS = 1e4;
784
776
  var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/RunEventStreamPortTag") {};
785
777
  const RunEventStreamPort = RunEventStreamPortTag;
@@ -891,12 +883,15 @@ const makeRunEventStream = (stdio, resolved, drainFramed = drainOf.bind(null, st
891
883
  });
892
884
  Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
893
885
  const TOOL_VARIABLES = ["CLAUDECODE", "CODEX_SANDBOX"];
894
- /**
895
- * The log colouriser's gate (R8). Machine mode never emits colour, so a
896
- * harness merging `2>&1` is not handed escape sequences it must strip, and
897
- * `NO_COLOR` is honoured for the human path per the convention: any value
898
- * other than an unset or empty variable disables colour.
899
- */
886
+ const decisionToResolvedMode = (decision) => Match.value(decision).pipe(Match.tag("HumanOutput", (human) => ({
887
+ mode: "human",
888
+ signal: human.signal,
889
+ stdoutIsTTY: human.stdoutIsTTY
890
+ })), Match.tag("MachineOutput", (machine) => ({
891
+ mode: "machine",
892
+ signal: machine.signal,
893
+ stdoutIsTTY: machine.stdoutIsTTY
894
+ })), Match.exhaustive);
900
895
  function isColorEnabled(resolved, noColor) {
901
896
  if (resolved.mode !== "human") return false;
902
897
  if (noColor === void 0) return true;
@@ -905,83 +900,80 @@ function isColorEnabled(resolved, noColor) {
905
900
  }
906
901
  var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/OutputModeProbeTag") {};
907
902
  const OutputModeProbe = OutputModeProbeTag;
908
- const outputModeProbeDescription = pipe(Cell.read((command) => Effect.succeed((() => {
909
- const toolVarsRecord = {};
910
- for (const variable of TOOL_VARIABLES) {
911
- const value = process.env[variable];
912
- if (value !== void 0) toolVarsRecord[variable] = value;
913
- }
914
- const envMode = process.env["STRYKER_MODE"];
915
- const agent = process.env["AGENT"];
916
- let result = { stdoutIsTTY: process.stdout.isTTY === true };
917
- if (Object.keys(toolVarsRecord).length > 0) result = {
918
- ...result,
919
- toolVars: toolVarsRecord
920
- };
921
- if (envMode !== void 0) result = {
922
- ...result,
923
- envMode
924
- };
925
- if (agent !== void 0) result = {
926
- ...result,
927
- agent
928
- };
929
- if (command.text !== void 0) result = {
930
- ...result,
931
- text: command.text
932
- };
933
- if (command.json !== void 0) result = {
934
- ...result,
935
- json: command.json
936
- };
937
- return result;
938
- })())), Cell.decode((raw) => Result.succeed((() => {
939
- const filteredToolVars = {};
940
- if (raw.toolVars !== void 0) {
941
- for (const [key, value] of Object.entries(raw.toolVars)) if (value !== void 0) filteredToolVars[key] = value;
942
- }
943
- let commandInput = { stdoutIsTTY: raw.stdoutIsTTY };
944
- if (raw.text !== void 0) commandInput = {
945
- ...commandInput,
946
- text: raw.text
947
- };
948
- if (raw.json !== void 0) commandInput = {
949
- ...commandInput,
950
- json: raw.json
951
- };
952
- if (raw.envMode !== void 0) commandInput = {
953
- ...commandInput,
954
- envMode: raw.envMode
955
- };
956
- if (raw.agent !== void 0) commandInput = {
957
- ...commandInput,
958
- agent: raw.agent
959
- };
960
- if (Object.keys(filteredToolVars).length > 0) commandInput = {
961
- ...commandInput,
962
- toolVars: filteredToolVars
963
- };
964
- return ResolveModeCommand.make(commandInput);
965
- })())), Cell.decide(resolveModeWorkflow), Cell.encode((outcome) => outcome), Cell.write((outcome) => Result.match(outcome, {
966
- onFailure: (error) => Effect.fail(error),
967
- onSuccess: (mode) => Effect.succeed(mode)
968
- })));
969
- const detectModeWithProbe = (flags = {}) => Cell.apply(outputModeProbeDescription, flags).pipe(Effect.mapError((error) => CliError.InvalidValue.make({
903
+ const outputModeProbeCell = Cell.layer({
904
+ read: (command) => Effect.succeed((() => {
905
+ const toolVarsRecord = {};
906
+ for (const variable of TOOL_VARIABLES) {
907
+ const value = process.env[variable];
908
+ if (value !== void 0) toolVarsRecord[variable] = value;
909
+ }
910
+ const envMode = process.env["STRYKER_MODE"];
911
+ const agent = process.env["AGENT"];
912
+ let result = { stdoutIsTTY: process.stdout.isTTY === true };
913
+ if (Object.keys(toolVarsRecord).length > 0) result = {
914
+ ...result,
915
+ toolVars: toolVarsRecord
916
+ };
917
+ if (envMode !== void 0) result = {
918
+ ...result,
919
+ envMode
920
+ };
921
+ if (agent !== void 0) result = {
922
+ ...result,
923
+ agent
924
+ };
925
+ if (command.text !== void 0) result = {
926
+ ...result,
927
+ text: command.text
928
+ };
929
+ if (command.json !== void 0) result = {
930
+ ...result,
931
+ json: command.json
932
+ };
933
+ return result;
934
+ })()),
935
+ decode: (raw) => Result.succeed((() => {
936
+ const filteredToolVars = {};
937
+ if (raw.toolVars !== void 0) {
938
+ for (const [key, value] of Object.entries(raw.toolVars)) if (value !== void 0) filteredToolVars[key] = value;
939
+ }
940
+ let commandInput = { stdoutIsTTY: raw.stdoutIsTTY };
941
+ if (raw.text !== void 0) commandInput = {
942
+ ...commandInput,
943
+ text: raw.text
944
+ };
945
+ if (raw.json !== void 0) commandInput = {
946
+ ...commandInput,
947
+ json: raw.json
948
+ };
949
+ if (raw.envMode !== void 0) commandInput = {
950
+ ...commandInput,
951
+ envMode: raw.envMode
952
+ };
953
+ if (raw.agent !== void 0) commandInput = {
954
+ ...commandInput,
955
+ agent: raw.agent
956
+ };
957
+ if (Object.keys(filteredToolVars).length > 0) commandInput = {
958
+ ...commandInput,
959
+ toolVars: filteredToolVars
960
+ };
961
+ return ResolveModeCommand.make(commandInput);
962
+ })()),
963
+ decide: resolveOutputMode,
964
+ encode: (outcome) => Result.map(outcome, decisionToResolvedMode),
965
+ write: (outcome) => Result.match(outcome, {
966
+ onFailure: (error) => Effect.fail(error),
967
+ onSuccess: (mode) => Effect.succeed(mode)
968
+ })
969
+ });
970
+ const detectModeWithProbe = (flags = {}) => Cell.run(outputModeProbeCell, flags).pipe(Effect.mapError((error) => CliError.InvalidValue.make({
970
971
  option: error.option,
971
972
  value: error.value,
972
973
  expected: error.expected,
973
974
  kind: "flag"
974
975
  })));
975
976
  const OutputModeProbeLive = Layer.succeed(OutputModeProbe, OutputModeProbe.of({ detectMode: detectModeWithProbe({}) }));
976
- /**
977
- * Machine mode emits the U4 verdict envelope for a run that produced no
978
- * mutants and no report file: a `--survivors` run with zero survivors (AE3)
979
- * or a successful `--dryRunOnly` run that ended before the mutation
980
- * pipeline. The envelope carries a null score and an empty mutant list and is
981
- * written as the terminal `verdict` line of the stdout stream (U6), carrying
982
- * the run id the stream header already opened with (KTD11 — never a fresh
983
- * id). Human mode prints nothing (the sink drops in human mode).
984
- */
985
977
  function emitNullScoreVerdict(stream, mode, thresholds, config, basePath, pathService) {
986
978
  const envelope = buildVerdictEnvelope({
987
979
  schemaVersion: "1.0",
@@ -1006,49 +998,41 @@ function emitNullScoreVerdict(stream, mode, thresholds, config, basePath, pathSe
1006
998
  mutants: envelope.mutants
1007
999
  }));
1008
1000
  }
1009
- /**
1010
- * Emits the machine-mode output from the run's finalizer — it runs on
1011
- * success, failure and interruption alike (R30): a failed run writes the
1012
- * `error` terminal event as the last line of the stdout stream; a successful
1013
- * run whose only console output was the framework's help/version rendering
1014
- * emits that captured document as the `help` terminal event, so `--help` in
1015
- * machine mode never leaks an ANSI document. A successful run with an empty
1016
- * buffer (the normal verdict path) emits nothing extra — the run already
1017
- * wrote its terminal `verdict` line through the same module — unless the
1018
- * stream is still open, which means the run never reached a verdict (the
1019
- * `--dryRunOnly` early return): then a null-score `verdict` closes the
1020
- * stream so the last stdout line is always a terminal event (R5).
1021
- */
1001
+ function offerFailureEnvelope(stream, failed, captured) {
1002
+ const envelope = shapeEnvelope(failed, captured);
1003
+ return Queue.offer(stream.queue, RunFailed.make({
1004
+ schemaVersion: envelope.schemaVersion,
1005
+ code: envelope.code,
1006
+ error: envelope.error,
1007
+ remediation: envelope.remediation
1008
+ }));
1009
+ }
1022
1010
  function emitMachineModeOutput(stream, mode, outcome, basePath, pathService) {
1023
1011
  return Effect.gen(function* () {
1024
1012
  const captured = readCapturedConsole();
1025
1013
  if (Result.isSuccess(outcome)) {
1026
- if (outcome.success.help) {
1027
- yield* Queue.offer(stream.queue, HelpRendered.make({
1028
- schemaVersion: "1.0",
1029
- code: 0,
1030
- help: captured
1031
- }));
1032
- return;
1033
- }
1034
- if (captured.length > 0) {
1035
- yield* Queue.offer(stream.queue, HelpRendered.make({
1036
- schemaVersion: "1.0",
1037
- code: 0,
1038
- help: captured
1039
- }));
1040
- return;
1041
- }
1042
- if (stream.isOpen()) yield* emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
1043
- return;
1014
+ const decision = outcome.success;
1015
+ return yield* Match.value(decision).pipe(Match.tag("RunOk", (ok) => Effect.gen(function* () {
1016
+ if (ok.help) {
1017
+ yield* Queue.offer(stream.queue, HelpRendered.make({
1018
+ schemaVersion: "1.0",
1019
+ code: 0,
1020
+ help: captured
1021
+ }));
1022
+ return;
1023
+ }
1024
+ if (captured.length > 0) {
1025
+ yield* Queue.offer(stream.queue, HelpRendered.make({
1026
+ schemaVersion: "1.0",
1027
+ code: 0,
1028
+ help: captured
1029
+ }));
1030
+ return;
1031
+ }
1032
+ if (stream.isOpen()) yield* emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
1033
+ })), Match.tag("RunParseFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunSurvivorsRejected", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunConfigFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.exhaustive);
1044
1034
  }
1045
- const envelope = shapeEnvelope(outcome.failure, captured);
1046
- yield* Queue.offer(stream.queue, RunFailed.make({
1047
- schemaVersion: envelope.schemaVersion,
1048
- code: envelope.code,
1049
- error: envelope.error,
1050
- remediation: envelope.remediation
1051
- }));
1035
+ return yield* offerFailureEnvelope(stream, outcome.failure, captured);
1052
1036
  });
1053
1037
  }
1054
1038
  //#endregion
@@ -1087,7 +1071,7 @@ const RunEventStreamFileLive = Layer.effect(RunEventStreamPort, Effect.gen(funct
1087
1071
  *
1088
1072
  * The prior-report decoding, source hashing, mutant conversion, and admission
1089
1073
  * pipeline for --survivors runs. Pure admission decision lives in
1090
- * Survivors.workflow.ts.
1074
+ * admit-survivors-run.workflow.ts.
1091
1075
  */
1092
1076
  const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
1093
1077
  /**
@@ -1183,87 +1167,57 @@ function survivorMutateSpans(survivors, basePath) {
1183
1167
  return spans;
1184
1168
  }
1185
1169
  const hashContent = (content) => bytesToHex(sha256(utf8ToBytes(content)));
1186
- /**
1187
- * The survivors admission, as a description whose phases chain by type and
1188
- * read in the order they run. The read gathers the admission's whole input
1189
- * product resolved options, prior report and the current source hashes —
1190
- * across its interior and stashes the shell context the write dispatches on
1191
- * into the executor-owned `runContext` ref; `decode` packages exactly the
1192
- * workflow input; `admitSurvivorsRun` is the decide phase; `encode` is the
1193
- * identity because write receives the outcome as-is; the write reads the
1194
- * stashed context back and dispatches the decision to the verdict/run,
1195
- * failing the run with a rejection.
1196
- */
1197
- const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext, basePath) => pipe(Cell.read((cliOptions) => Effect.flatMap(Path.Path, (pathService) => resolveSurvivorsRunOptions(cliOptions, basePath).pipe(Effect.flatMap((resolvedOptions) => {
1198
- const priorReportPath = priorReportPathOf(resolvedOptions);
1199
- const resolveAbsolutePath = (file) => pathService.resolve(file);
1200
- return Effect.flatMap(readPriorReport(priorReportPath), (read) => Effect.flatMap(currentSourceHashesFor(priorReportFileKeys(read.raw)), (sourceContentHashes) => Ref.set(runContext, {
1201
- resolvedOptions,
1202
- priorReportPath,
1203
- pathService
1204
- }).pipe(Effect.as({
1205
- resolvedOptions,
1206
- priorReportRaw: read.raw,
1207
- priorReportFound: read.found,
1208
- priorReportPath,
1209
- sourceContentHashes,
1210
- resolveAbsolutePath
1211
- }))));
1212
- }))).pipe(Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer, nodeModuleLayer)))), Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes, resolveAbsolutePath }) => {
1213
- if (!priorReportFound) return Result.succeed(AdmitSurvivorsRunCommand.make({
1214
- priorReport: void 0,
1215
- currentConfig: resolvedOptions,
1216
- frameworkVersion: strykerVersion,
1217
- sourceContentHashes,
1218
- priorSourceHashes: {},
1219
- priorSurvivors: []
1220
- }));
1221
- return Result.map(decodePriorReport(priorReportRaw), (document) => AdmitSurvivorsRunCommand.make({
1222
- priorReport: PriorReportFacts.make({
1223
- config: document.config ?? {},
1224
- frameworkVersion: document.framework?.version
1170
+ const survivorsAdmissionCell = (basePath) => Cell.layer({
1171
+ read: (cliOptions) => Effect.gen(function* () {
1172
+ const pathService = yield* Path.Path;
1173
+ const resolvedOptions = yield* resolveSurvivorsRunOptions(cliOptions, basePath);
1174
+ const priorReportPath = priorReportPathOf(resolvedOptions);
1175
+ const resolveAbsolutePath = (file) => pathService.resolve(file);
1176
+ const read = yield* readPriorReport(priorReportPath);
1177
+ const sourceContentHashes = yield* currentSourceHashesFor(priorReportFileKeys(read.raw));
1178
+ return {
1179
+ resolvedOptions,
1180
+ priorReportRaw: read.raw,
1181
+ priorReportFound: read.found,
1182
+ priorReportPath,
1183
+ sourceContentHashes,
1184
+ resolveAbsolutePath
1185
+ };
1186
+ }),
1187
+ decode: ({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes, resolveAbsolutePath }) => {
1188
+ if (!priorReportFound) return Result.succeed(AdmitSurvivorsRunCommand.make({
1189
+ priorReport: void 0,
1190
+ currentConfig: resolvedOptions,
1191
+ frameworkVersion: strykerVersion,
1192
+ sourceContentHashes,
1193
+ priorSourceHashes: {},
1194
+ priorSurvivors: []
1195
+ }));
1196
+ return Result.map(decodePriorReport(priorReportRaw), (document) => AdmitSurvivorsRunCommand.make({
1197
+ priorReport: PriorReportFacts.make({
1198
+ config: document.config ?? {},
1199
+ frameworkVersion: document.framework?.version
1200
+ }),
1201
+ currentConfig: resolvedOptions,
1202
+ frameworkVersion: strykerVersion,
1203
+ sourceContentHashes,
1204
+ priorSourceHashes: priorSourceHashes(document, hashContent),
1205
+ priorSurvivors: extractSurvivors(document, resolveAbsolutePath)
1206
+ }));
1207
+ },
1208
+ decide: admitSurvivorsRun,
1209
+ encode: (outcome) => outcome,
1210
+ write: (outcome, raw) => Result.match(outcome, {
1211
+ onSuccess: (admission) => Effect.succeed({
1212
+ admission,
1213
+ resolvedOptions: raw.resolvedOptions,
1214
+ priorReportPath: raw.priorReportPath
1225
1215
  }),
1226
- currentConfig: resolvedOptions,
1227
- frameworkVersion: strykerVersion,
1228
- sourceContentHashes,
1229
- priorSourceHashes: priorSourceHashes(document, hashContent),
1230
- priorSurvivors: extractSurvivors(document, resolveAbsolutePath)
1231
- }));
1232
- }), Cell.decide(admitSurvivorsRun), Cell.encode((outcome) => outcome), Cell.write((outcome) => Effect.flatMap(Ref.get(runContext), (context) => {
1233
- if (context === void 0) return Effect.die("the survivors admission read must run before its write");
1234
- const { resolvedOptions, priorReportPath, pathService } = context;
1235
- return Result.match(outcome, {
1236
- onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () => emitNullScoreVerdict(stream, mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService)), Match.tag("Admitted", (admitted) => {
1237
- const admittedMutants = admitted.survivors.map((s) => Mutant.make(s));
1238
- return runMutationTest({
1239
- ...resolvedOptions,
1240
- survivors: admittedMutants,
1241
- mutate: survivorMutateSpans(admittedMutants, basePath),
1242
- survivorsPriorReport: priorReportPath,
1243
- incremental: false
1244
- }).pipe(Effect.orDie);
1245
- }), Match.orElse(() => Effect.die("unreachable admission decision variant"))),
1246
- onFailure: (rejection) => Effect.fail(rejection)
1247
- });
1248
- })));
1249
- /**
1250
- * The `--survivors` request: re-test exactly the prior report's survivor set.
1251
- * The survivors flag was parsed as a boolean; the admission decides between
1252
- * running the survivors and the plain pipeline. The chain's order is carried by
1253
- * the description's phase types; the run's resolved context cell is created
1254
- * here, beside the description it feeds.
1255
- *
1256
- * Two failures reach the caller, and they are not the same thing. A rejection is the
1257
- * decision's own outcome — the run was inspected and refused. A `SchemaError` is a prior
1258
- * report that was present and did not decode, which stops the chain before any decision
1259
- * is made; it is in this signature because the phase types put it there, not because the
1260
- * admission chose it.
1261
- */
1262
- function runSurvivorsAdmission(runMutationTest, stream, mode, cliOptions, basePath) {
1263
- return Effect.gen(function* () {
1264
- const admissionContext = yield* Ref.make(void 0);
1265
- return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext, basePath), cliOptions);
1266
- });
1216
+ onFailure: Effect.fail
1217
+ })
1218
+ });
1219
+ function runSurvivorsAdmission(cliOptions, basePath) {
1220
+ return Cell.run(survivorsAdmissionCell(basePath), cliOptions);
1267
1221
  }
1268
1222
  function resolveSurvivorsRunOptions(cliOptions, basePath) {
1269
1223
  return readConfig(cliOptions, basePath);
@@ -1515,7 +1469,7 @@ function readCoreEntries() {
1515
1469
  return Effect.gen(function* () {
1516
1470
  const path = yield* Path.Path;
1517
1471
  const fs = yield* FileSystem.FileSystem;
1518
- const resolved = import.meta.resolve("@systemfsoftware/stryker-js-platform-node/package.json");
1472
+ const resolved = import.meta.resolve("@systemfsoftware/stryker-js-engine/package.json");
1519
1473
  const manifestPath = yield* path.fromFileUrl(new URL(resolved));
1520
1474
  const raw = yield* fs.readFileString(manifestPath);
1521
1475
  let parsed;
@@ -1530,8 +1484,6 @@ function readCoreEntries() {
1530
1484
  return [];
1531
1485
  }).pipe(Effect.catchCause(() => {
1532
1486
  return Effect.succeed([]);
1533
- }), Effect.catchDefect(() => {
1534
- return Effect.succeed([]);
1535
1487
  }));
1536
1488
  }
1537
1489
  function buildLLMSManifest(command, version) {
@@ -1765,7 +1717,7 @@ const cliLayer = Layer.mergeAll(CliConfig.layer({ builtIns: [
1765
1717
  GlobalFlag.Wizard,
1766
1718
  GlobalFlag.Completions,
1767
1719
  GlobalFlag.LogLevel
1768
- ] }), Path.layer, NodeFileSystem$1.layer, terminalLayer, NodeStdio.layer, NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem$1.layer, NodePath$1.layer))));
1720
+ ] }), Path.layer, NodeFileSystem.layer, terminalLayer, NodeStdio.layer, NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))));
1769
1721
  function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStream, lastSignal) {
1770
1722
  return Effect.gen(function* () {
1771
1723
  const mode = yield* detectMode;
@@ -1788,14 +1740,15 @@ function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStrea
1788
1740
  return result.success;
1789
1741
  }).pipe(Effect.orElseSucceed(() => 2));
1790
1742
  }
1791
- const defaultRunMutationTest = (hostOptions, queue) => (options) => Effect.scoped(runMutationTest(options, makeRunLayer(hostOptions, queue))).pipe(Effect.provideService(RunEvents, queue));
1743
+ const hostRunLayer = (hostOptions, queue) => makeRunLayer(hostOptions, queue).pipe(Layer.provideMerge(nodePlatformLayer));
1744
+ const defaultRunMutationTest = (hostOptions, queue) => (...args) => Effect.scoped(runMutationTest(...args)).pipe(Effect.provide(hostRunLayer(hostOptions, queue)), Effect.provideService(RunEvents, queue));
1792
1745
  function hostOptionsOf(mode, stream) {
1793
1746
  return {
1794
1747
  runId: stream.runId,
1795
1748
  resolvedMode: mode,
1796
1749
  runStartedAt: stream.startedAt,
1797
1750
  basePath: process.cwd(),
1798
- reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter"), import.meta.resolve("@systemfsoftware/stryker-js-platform-node/builtin-reporters")],
1751
+ reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter"), import.meta.resolve("@systemfsoftware/stryker-js-engine/builtin-reporters")],
1799
1752
  allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
1800
1753
  };
1801
1754
  }
@@ -1804,7 +1757,7 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1804
1757
  const hostOptions = hostOptionsOf(input.mode, stream);
1805
1758
  const runMutationTestImpl = input.runMutationTest ?? defaultRunMutationTest(hostOptions, stream.queue);
1806
1759
  const basePath = hostOptions.basePath;
1807
- const pathService = yield* Path.Path.pipe(Effect.provide(NodePath$1.layer));
1760
+ const pathService = yield* Path.Path.pipe(Effect.provide(NodePath.layer));
1808
1761
  let currentFiber = null;
1809
1762
  const onSignal = () => {
1810
1763
  process.removeListener("SIGINT", onSignal);
@@ -1812,7 +1765,20 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1812
1765
  if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
1813
1766
  };
1814
1767
  const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => (() => {
1815
- if (runRequest.survivors) return runSurvivorsAdmission(runMutationTestImpl, stream, input.mode, runRequest.options, basePath).pipe(Effect.provide(makeRunLayer(hostOptions)));
1768
+ if (runRequest.survivors) return Effect.gen(function* () {
1769
+ const { admission, resolvedOptions, priorReportPath } = yield* runSurvivorsAdmission(runRequest.options, basePath).pipe(Effect.provide(hostRunLayer(hostOptions)));
1770
+ return yield* Match.value(admission).pipe(Match.tag("NoSurvivors", () => emitNullScoreVerdict(stream, input.mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService)), Match.tag("Admitted", (admitted) => {
1771
+ const admittedMutants = admitted.survivors.map((s) => Mutant.make(s));
1772
+ const restricted = {
1773
+ ...resolvedOptions,
1774
+ survivors: admittedMutants,
1775
+ mutate: survivorMutateSpans(admittedMutants, basePath),
1776
+ survivorsPriorReport: priorReportPath,
1777
+ incremental: false
1778
+ };
1779
+ return runMutationTestImpl(restricted).pipe(Effect.orDie);
1780
+ }), Match.exhaustive);
1781
+ });
1816
1782
  return runMutationTestImpl(runRequest.options).pipe(Effect.orDie);
1817
1783
  })()), Match.tag("llms", (llmsRequest) => Effect.gen(function* () {
1818
1784
  stream.ensureOpen({
@@ -1866,29 +1832,18 @@ const EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER = 1;
1866
1832
  process.title = "stryker";
1867
1833
  const lastSignal = observeTerminatingSignal();
1868
1834
  function isSupportedNodeVersion(version) {
1869
- let withoutV = version;
1870
- if (version.startsWith("v")) withoutV = version.slice(1);
1871
- const dashBaseRaw = withoutV.split("-")[0];
1872
- let dashBase = withoutV;
1873
- if (dashBaseRaw !== void 0) dashBase = dashBaseRaw;
1874
- const baseRaw = dashBase.split("+")[0];
1875
- let base = dashBase;
1876
- if (baseRaw !== void 0) base = baseRaw;
1877
- const parts = base.split(".").map((p) => Number.parseInt(p, 10));
1878
- const major = parts[0] ?? 0;
1879
- const minor = parts[1] ?? 0;
1880
- const patch = parts[2] ?? 0;
1835
+ const [major = 0, minor = 0, patch = 0] = (version.replace(/^v/, "").split(/[-+]/)[0] ?? "").split(".").map((p) => Number.parseInt(p, 10));
1881
1836
  if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) return false;
1882
1837
  if (major !== 20) return major > 20;
1883
1838
  if (minor !== 0) return minor > 0;
1884
1839
  return patch >= 0;
1885
1840
  }
1886
- if (!isSupportedNodeVersion(process.version)) throw new Error(`Node.js version ${process.version} detected. StrykerJS requires version to match ${strykerEngines.node}. Please update your Node.js version or visit https://nodejs.org/ for additional instructions`);
1841
+ if (!isSupportedNodeVersion(process.version)) throw new Error(`Node.js version ${process.version} detected. StrykerJS requires version to match ${engines.node}. Please update your Node.js version or visit https://nodejs.org/ for additional instructions`);
1887
1842
  const program = Effect.gen(function* () {
1888
1843
  const outputMode = yield* OutputModeProbe;
1889
1844
  const runEvents = yield* RunEventStreamPort;
1890
1845
  return yield* strykerCliEffect(process.argv.slice(2), void 0, outputMode.detectMode, runEvents.createRunEventStream, lastSignal);
1891
- }).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamFileLive).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem$1.layer, NodePath$1.layer)))));
1846
+ }).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamFileLive).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem.layer, NodePath.layer)))));
1892
1847
  NodeRuntime.runMain(program, {
1893
1848
  disableErrorReporting: true,
1894
1849
  teardown: (exit, onExit) => {
@@ -0,0 +1,105 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
4
+ import { ChildProcessCrashedError, WorkerEntries, WorkerLauncher } from "@systemfsoftware/stryker-js-engine";
5
+ import * as FileSystem from "effect/FileSystem";
6
+ import * as Match from "effect/Match";
7
+ import * as Path from "effect/Path";
8
+ import { NodeFileSystem, NodePath, NodeSocket } from "@effect/platform-node";
9
+ import { Module } from "@systemfsoftware/stryker-js/Module";
10
+ import * as ChildProcess from "effect/unstable/process/ChildProcess";
11
+ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
12
+ import * as RpcClient from "effect/unstable/rpc/RpcClient";
13
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
14
+ //#region src/platform/node.ts
15
+ const makeModuleRequire = (nodeModule, filename) => {
16
+ const requireFrom = nodeModule.createRequire(filename);
17
+ const requireFn = (request) => requireFrom(request);
18
+ requireFn.resolve = (request, options) => {
19
+ if (options === void 0) return requireFrom.resolve(request);
20
+ return requireFrom.resolve(request, { paths: [...options.paths ?? []] });
21
+ };
22
+ return requireFn;
23
+ };
24
+ /**
25
+ * The Node implementation of the {@link Module} port: every call routes
26
+ * through the runtime's own `node:module` via `process.getBuiltinModule`, so
27
+ * this package imports no host builtins and the import ban holds here too.
28
+ */
29
+ const nodeModuleLayer = Layer.effect(Module, Effect.sync(() => {
30
+ const nodeModule = process.getBuiltinModule("node:module");
31
+ return {
32
+ createRequire: (filename) => makeModuleRequire(nodeModule, filename),
33
+ isBuiltin: (moduleName) => nodeModule.isBuiltin(moduleName)
34
+ };
35
+ }));
36
+ /**
37
+ * The worker entries this package's own build emits. The engine spawns by
38
+ * address; this process package knows its dist layout and hands the
39
+ * addresses in.
40
+ */
41
+ const workerEntriesLayer = Layer.succeed(WorkerEntries, {
42
+ checkerWorkerUrl: new URL("./workers/checker-worker.mjs", import.meta.url),
43
+ testRunnerWorkerUrl: new URL("./workers/child-process-test-runner-worker.mjs", import.meta.url)
44
+ });
45
+ /**
46
+ * The Node worker launcher: spawn a worker child with this runtime's
47
+ * executable, host the RPC server's address as a `net` `path` endpoint (a
48
+ * socket file in the worker directory on POSIX, a same-user named pipe on
49
+ * Windows), and connect the NDJSON protocol client over `NodeSocket`.
50
+ */
51
+ const nodeWorkerLauncherLayer = Layer.effect(WorkerLauncher, Effect.gen(function* () {
52
+ const fs = yield* FileSystem.FileSystem;
53
+ const path = yield* Path.Path;
54
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
55
+ return { spawn: (params) => Effect.gen(function* () {
56
+ const workerDir = yield* fs.makeTempDirectoryScoped({ prefix: params.tempDirPrefix });
57
+ const socketPath = Match.value(process.platform).pipe(Match.when("win32", () => `\\\\.\\pipe\\stryker-worker-${globalThis.crypto.randomUUID()}`), Match.orElse(() => path.join(workerDir, "worker.sock")));
58
+ yield* fs.writeFileString(path.join(workerDir, "options.json"), params.optionsJson);
59
+ const entryPath = yield* path.fromFileUrl(params.entryUrl);
60
+ const handle = yield* ChildProcess.make(process.execPath, [...params.execArgv, entryPath], {
61
+ cwd: params.workingDirectory,
62
+ extendEnv: true,
63
+ env: {
64
+ STRYKER_WORKER_DIR: workerDir,
65
+ STRYKER_SOCKET: socketPath
66
+ },
67
+ stderr: "inherit"
68
+ }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner));
69
+ const clientLayer = RpcClient.layerProtocolSocket({ retryTransientErrors: true }).pipe(Layer.provide(NodeSocket.layerNet({ path: socketPath })), Layer.provide(RpcSerialization.layerNdjson));
70
+ const exited = handle.exitCode.pipe(Effect.orDie, Effect.flatMap((exitCode) => Effect.fail(new ChildProcessCrashedError({
71
+ pid: Number(handle.pid),
72
+ exit: {
73
+ _tag: "Code",
74
+ code: exitCode
75
+ },
76
+ cause: "worker exited before it accepted the RPC connection"
77
+ }))));
78
+ return {
79
+ pid: Number(handle.pid),
80
+ clientLayer,
81
+ exited
82
+ };
83
+ }).pipe(Effect.catch((error) => {
84
+ if (error instanceof ChildProcessCrashedError) return Effect.fail(error);
85
+ return Effect.fail(new ChildProcessCrashedError({
86
+ pid: 0,
87
+ exit: {
88
+ _tag: "Code",
89
+ code: 1
90
+ },
91
+ cause: "worker spawn failed"
92
+ }));
93
+ })) };
94
+ }));
95
+ const nodeFsPathLayer = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer);
96
+ const nodeSpawnerLayer = NodeChildProcessSpawner.layer.pipe(Layer.provide(nodeFsPathLayer));
97
+ const nodeBase = Layer.merge(nodeFsPathLayer, nodeSpawnerLayer);
98
+ /**
99
+ * Every port the engine requires, provided from this runtime: the file
100
+ * system, the path service, the module loader, the child-process spawner,
101
+ * the worker launcher, and this build's worker entry addresses.
102
+ */
103
+ const nodePlatformLayer = Layer.mergeAll(nodeModuleLayer, workerEntriesLayer, nodeWorkerLauncherLayer.pipe(Layer.provide(nodeBase)), nodeBase);
104
+ //#endregion
105
+ export { nodePlatformLayer as n, nodeModuleLayer as t };
@@ -0,0 +1,20 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import * as Cause from "effect/Cause";
4
+ //#region src/workers/worker-runtime.ts
5
+ const workerSocketPath = (label) => {
6
+ const socketPath = process.env["STRYKER_SOCKET"];
7
+ if (socketPath === void 0) {
8
+ process.stderr.write(`${label}: STRYKER_SOCKET is not set\n`);
9
+ process.exit(1);
10
+ }
11
+ return socketPath;
12
+ };
13
+ const launchWorker = (main, label) => {
14
+ Effect.runFork(Layer.launch(main).pipe(Effect.tapCause((cause) => Effect.sync(() => {
15
+ process.stderr.write(`${label}: ${Cause.pretty(cause)}\n`);
16
+ process.exitCode = 1;
17
+ }))));
18
+ };
19
+ //#endregion
20
+ export { workerSocketPath as n, launchWorker as t };
@@ -0,0 +1,53 @@
1
+ import { t as nodeModuleLayer } from "../node-D-ynY_kk.mjs";
2
+ import { n as workerSocketPath, t as launchWorker } from "../worker-runtime-CT5LMMgY.mjs";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import * as FileSystem from "effect/FileSystem";
6
+ import * as Option from "effect/Option";
7
+ import * as Path from "effect/Path";
8
+ import { NodeFileSystem, NodePath, NodeSocketServer } from "@effect/platform-node";
9
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
10
+ import { Checker, CheckerFailed } from "@systemfsoftware/stryker-js/Checker";
11
+ import { RunConfiguration, SandboxDirectory } from "@systemfsoftware/stryker-js/Plugin";
12
+ import * as HashMap from "effect/HashMap";
13
+ import * as RpcServer from "effect/unstable/rpc/RpcServer";
14
+ import { CheckerRpcs, create, decodeWorkerOptions, loadPlugins } from "@systemfsoftware/stryker-js-engine/worker";
15
+ //#region src/workers/Checker.worker.ts
16
+ const buildChecker = (contribution, options) => Checker.pipe(Effect.provide(contribution.layer.pipe(Layer.provide(Layer.mergeAll(Layer.succeed(RunConfiguration, options), Layer.succeed(SandboxDirectory, process.cwd()), NodeFileSystem.layer, NodePath.layer, nodeModuleLayer)))));
17
+ const mutantIdsOf = (mutants) => mutants.map((mutant) => mutant.id);
18
+ const readWorkerOptions = Effect.gen(function* () {
19
+ const workerDir = process.env["STRYKER_WORKER_DIR"] ?? (yield* Effect.die(/* @__PURE__ */ new Error("STRYKER_WORKER_DIR is not set")));
20
+ const fs = yield* FileSystem.FileSystem;
21
+ const path = yield* Path.Path;
22
+ const raw = yield* fs.readFileString(path.join(workerDir, "options.json"));
23
+ return yield* decodeWorkerOptions(raw);
24
+ });
25
+ const CheckerHandlers = CheckerRpcs.toLayer(Effect.gen(function* () {
26
+ const options = yield* readWorkerOptions;
27
+ const loaded = yield* loadPlugins(options.plugins, process.cwd());
28
+ const checkers = HashMap.fromIterable(yield* Effect.forEach(options.checkers, (name) => Effect.gen(function* () {
29
+ const contribution = yield* create(loaded.pluginsByKind, "Checker", name);
30
+ const checker = yield* buildChecker(contribution, options);
31
+ yield* checker.init;
32
+ return [name, checker];
33
+ }), {
34
+ concurrency: "unbounded",
35
+ discard: false
36
+ }));
37
+ const resolve = (checkerName, mutants) => Option.match(HashMap.get(checkers, checkerName), {
38
+ onNone: () => Effect.fail(new CheckerFailed({
39
+ cause: `Checker ${checkerName} does not exist`,
40
+ checkerName,
41
+ mutantIds: mutantIdsOf(mutants)
42
+ })),
43
+ onSome: (checker) => Effect.succeed(checker)
44
+ });
45
+ return {
46
+ check: ({ checkerName, mutants }) => resolve(checkerName, mutants).pipe(Effect.flatMap((checker) => checker.check([...mutants])), Effect.map((resultMap) => Object.fromEntries(resultMap))),
47
+ group: ({ checkerName, mutants }) => resolve(checkerName, mutants).pipe(Effect.flatMap((checker) => checker.group([...mutants])))
48
+ };
49
+ })).pipe(Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
50
+ const MainLayer = RpcServer.layer(CheckerRpcs).pipe(Layer.provide(CheckerHandlers), Layer.provide(RpcServer.layerProtocolSocketServer), Layer.provide(RpcSerialization.layerNdjson), Layer.provide(NodeSocketServer.layer({ path: workerSocketPath("checker worker") })), Layer.provide(nodeModuleLayer));
51
+ launchWorker(MainLayer, "checker worker");
52
+ //#endregion
53
+ export {};
@@ -0,0 +1,67 @@
1
+ import { t as nodeModuleLayer } from "../node-D-ynY_kk.mjs";
2
+ import { n as workerSocketPath, t as launchWorker } from "../worker-runtime-CT5LMMgY.mjs";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import { errorToString } from "@systemfsoftware/stryker-js/Mutant";
6
+ import * as Cause from "effect/Cause";
7
+ import * as FileSystem from "effect/FileSystem";
8
+ import * as Path from "effect/Path";
9
+ import { NodeFileSystem, NodePath, NodeSocketServer } from "@effect/platform-node";
10
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
11
+ import { RunConfiguration, SandboxDirectory } from "@systemfsoftware/stryker-js/Plugin";
12
+ import * as RpcServer from "effect/unstable/rpc/RpcServer";
13
+ import { MutantCoverageSchema, TestRunnerRpcs, create, decodeWorkerOptions, loadPlugins } from "@systemfsoftware/stryker-js-engine/worker";
14
+ import { TestRunner, TestRunnerFailed } from "@systemfsoftware/stryker-js/TestRunner";
15
+ import { Schema } from "effect";
16
+ //#region src/workers/child-process-test-runner-worker.ts
17
+ const withCoverage = (result, options) => Effect.gen(function* () {
18
+ if (result.status === "error") return {
19
+ ...result,
20
+ errorMessage: errorToString(result.errorMessage)
21
+ };
22
+ if (result.status !== "complete") return result;
23
+ if (result.mutantCoverage !== void 0) return result;
24
+ if (options.coverageAnalysis === "off") return result;
25
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.optional(MutantCoverageSchema))(globalThis.__mutantCoverage__).pipe(Effect.orElseSucceed(() => void 0));
26
+ if (decoded === void 0) return result;
27
+ return {
28
+ ...result,
29
+ mutantCoverage: decoded
30
+ };
31
+ });
32
+ const normalizeMutantRun = (result) => {
33
+ if (result.status === "error") return {
34
+ ...result,
35
+ errorMessage: errorToString(result.errorMessage)
36
+ };
37
+ return result;
38
+ };
39
+ const readWorkerOptions = Effect.gen(function* () {
40
+ const workerDir = process.env["STRYKER_WORKER_DIR"] ?? (yield* Effect.die(/* @__PURE__ */ new Error("STRYKER_WORKER_DIR is not set")));
41
+ const fs = yield* FileSystem.FileSystem;
42
+ const path = yield* Path.Path;
43
+ const raw = yield* fs.readFileString(path.join(workerDir, "options.json"));
44
+ return yield* decodeWorkerOptions(raw);
45
+ });
46
+ const TestRunnerHandlers = TestRunnerRpcs.toLayer(Effect.gen(function* () {
47
+ const options = yield* readWorkerOptions;
48
+ const runnerName = options.testRunner;
49
+ const failed = (phase) => (cause) => Effect.fail(new TestRunnerFailed({
50
+ cause: Cause.pretty(cause),
51
+ phase,
52
+ runnerName
53
+ }));
54
+ const loaded = yield* loadPlugins(options.plugins, process.cwd()).pipe(Effect.catchCause(failed("init")));
55
+ const underlying = yield* create(loaded.pluginsByKind, "TestRunner", runnerName).pipe(Effect.flatMap((contribution) => TestRunner.pipe(Effect.provide(contribution.layer))), Effect.provide(Layer.merge(Layer.succeed(RunConfiguration, options), Layer.succeed(SandboxDirectory, process.cwd()))), Effect.catchCause(failed("init")));
56
+ yield* underlying.init.pipe(Effect.catchCause(failed("init")));
57
+ yield* Effect.addFinalizer(() => underlying.dispose.pipe(Effect.ignore));
58
+ return {
59
+ capabilities: () => underlying.capabilities.pipe(Effect.catchCause(failed("capabilities"))),
60
+ dryRun: ({ options: runOptions }) => underlying.dryRun(runOptions).pipe(Effect.flatMap((result) => withCoverage(result, runOptions)), Effect.catchCause(failed("dryRun"))),
61
+ mutantRun: ({ options: runOptions }) => underlying.mutantRun(runOptions).pipe(Effect.map(normalizeMutantRun), Effect.catchCause(failed("mutantRun")))
62
+ };
63
+ })).pipe(Layer.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)));
64
+ const MainLayer = RpcServer.layer(TestRunnerRpcs).pipe(Layer.provide(TestRunnerHandlers), Layer.provide(RpcServer.layerProtocolSocketServer), Layer.provide(RpcSerialization.layerNdjson), Layer.provide(NodeSocketServer.layer({ path: workerSocketPath("test runner worker") })), Layer.provide(nodeModuleLayer));
65
+ launchWorker(MainLayer, "test runner worker");
66
+ //#endregion
67
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemfsoftware/stryker-js-cli",
3
- "version": "4.0.3",
3
+ "version": "5.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
@@ -18,32 +18,33 @@
18
18
  "@effect/platform-node-shared": "^4.0.0-rc.112",
19
19
  "@noble/hashes": "1.8.0",
20
20
  "effect": "^4.0.0-rc.112",
21
- "@systemfsoftware/effect-cell-types": "^5.0.2",
22
- "@systemfsoftware/stryker-js-html-reporter": "^0.1.2",
23
- "@systemfsoftware/stryker-js-platform-node": "^0.2.0",
24
- "@systemfsoftware/stryker-js": "^0.2.0"
21
+ "@systemfsoftware/effect-cell-types": "^6.0.0",
22
+ "@systemfsoftware/stryker-js-engine": "^0.2.0",
23
+ "@systemfsoftware/stryker-js": "^1.0.0",
24
+ "@systemfsoftware/stryker-js-html-reporter": "^1.0.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@systemfsoftware/arethetypeswrong-cli": "^1.1.1",
28
28
  "@types/node": "^24",
29
29
  "@vitest/coverage-v8": "^4",
30
+ "mutation-testing-report-schema": "3.7.3",
30
31
  "oxlint": "^1.77.0",
31
32
  "rimraf": "^6.1.3",
32
33
  "testcontainers": "^12.1.0",
33
34
  "tsdown": "^0.22.14",
34
35
  "vite-tsconfig-paths": "^6.1.1",
35
36
  "vitest": "^4",
37
+ "@systemfsoftware/all": "^1.1.2",
36
38
  "@systemfsoftware/effect-gherkin-spec": "^4.0.0",
37
39
  "@systemfsoftware/effect-schema-law": "^2.0.1",
38
40
  "@systemfsoftware/effect-schema-vite": "^2.0.2",
39
- "@systemfsoftware/all": "^1.1.0",
40
- "@systemfsoftware/oxlint-config": "^0.1.0",
41
- "@systemfsoftware/stryker-js-typescript-checker": "^3.0.3",
42
- "@systemfsoftware/stryker-js-vitest-runner": "^2.1.0",
43
- "@systemfsoftware/stryker-plugins": "^2.0.1",
44
- "@systemfsoftware/tsconfig": "^1.3.3",
41
+ "@systemfsoftware/stryker-js-typescript-checker": "^4.0.0",
42
+ "@systemfsoftware/stryker-js-vitest-runner": "^3.0.0",
43
+ "@systemfsoftware/stryker-plugins": "^2.0.2",
44
+ "@systemfsoftware/stryker-test-contribution": "^1.1.3",
45
45
  "@systemfsoftware/vitest-config": "^0.1.0",
46
- "@systemfsoftware/stryker-test-contribution": "^1.1.2"
46
+ "@systemfsoftware/tsconfig": "^1.3.3",
47
+ "@systemfsoftware/oxlint-config": "^0.1.0"
47
48
  },
48
49
  "engines": {
49
50
  "node": ">=20.0.0"