@systemfsoftware/stryker-js-cli 3.2.0 → 4.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/dist/main.mjs CHANGED
@@ -1,303 +1,123 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
4
+ import * as NodePath from "@effect/platform-node-shared/NodePath";
2
5
  import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
3
6
  import * as NodeStdio from "@effect/platform-node/NodeStdio";
7
+ import { ConfigFileUnreadableError, buildVerdictEnvelope, defaultOptions, generateRunId, makeRunLayer, readConfig, runMutationTest, strykerEngines, strykerVersion, toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-platform-node";
4
8
  import * as Effect from "effect/Effect";
5
9
  import * as Exit from "effect/Exit";
6
10
  import * as Layer from "effect/Layer";
7
11
  import * as Logger from "effect/Logger";
8
- import { ExitClass, buildVerdictEnvelope, defaultOptions, defaultStages, forkCoreSchema, generateRunId, highestExitClass, makeRunLayer, readConfig, resolveExitCode, runMutationTest, strykerEngines, strykerVersion, toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-mutation-run";
9
- import * as Context from "effect/Context";
10
- import * as Result from "effect/Result";
11
- import * as CliError from "effect/unstable/cli/CliError";
12
- import * as Cause from "effect/Cause";
13
- import * as Clock from "effect/Clock";
14
- import * as Deferred from "effect/Deferred";
15
- import * as Fiber from "effect/Fiber";
16
- import * as Queue from "effect/Queue";
17
- import * as Stdio from "effect/Stdio";
18
- import * as Stream from "effect/Stream";
19
12
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
20
- import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
21
- import * as NodePath$1 from "@effect/platform-node-shared/NodePath";
22
- import { RENDERED_OPTION_DEFAULTS, causeText } from "@systemfsoftware/stryker-js-plugin-api/core";
13
+ import { Heartbeat, HelpRendered, ManifestRendered, RunEvents, RunFailed, RunStarted, VerdictReached } from "@systemfsoftware/stryker-js/Run";
14
+ import { RENDERED_OPTION_DEFAULTS } from "@systemfsoftware/stryker-js/Schema";
15
+ import * as Cause from "effect/Cause";
23
16
  import * as Console from "effect/Console";
17
+ import * as Fiber from "effect/Fiber";
24
18
  import * as FileSystem from "effect/FileSystem";
19
+ import * as Match from "effect/Match";
25
20
  import * as Option from "effect/Option";
26
21
  import * as Path from "effect/Path";
22
+ import * as Queue from "effect/Queue";
27
23
  import * as Ref from "effect/Ref";
24
+ import * as Result from "effect/Result";
25
+ import * as S from "effect/Schema";
28
26
  import * as Terminal from "effect/Terminal";
29
27
  import * as Argument from "effect/unstable/cli/Argument";
30
28
  import * as CliConfig from "effect/unstable/cli/CliConfig";
29
+ import * as CliError from "effect/unstable/cli/CliError";
31
30
  import * as Command from "effect/unstable/cli/Command";
32
31
  import * as Flag from "effect/unstable/cli/Flag";
33
32
  import * as GlobalFlag from "effect/unstable/cli/GlobalFlag";
33
+ import { readFileSync } from "node:fs";
34
34
  import { resolve } from "node:path";
35
- import { NodePath } from "@effect/platform-node";
36
- import * as Match from "effect/Match";
35
+ import { ExitClass, highestExitClass } from "@systemfsoftware/stryker-js/ExitClass";
36
+ import { Mutant, causeText } from "@systemfsoftware/stryker-js/Mutant";
37
+ import * as Clock from "effect/Clock";
38
+ import * as Formatter from "effect/Formatter";
37
39
  import * as Predicate from "effect/Predicate";
38
- import * as S from "effect/Schema";
39
40
  import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
40
- import { performance } from "node:perf_hooks";
41
- import { format, inspect } from "node:util";
42
- import { createHash } from "node:crypto";
43
- import { readFileSync } from "node:fs";
44
- import { noopLogger } from "@systemfsoftware/stryker-js-plugin-api/logging";
41
+ import * as Context from "effect/Context";
45
42
  import { pipe } from "effect/Function";
46
- import { PluginKind } from "@systemfsoftware/stryker-js-plugin-api/plugin";
47
- import { strykerPlugins } from "@systemfsoftware/stryker-js-mutation-report/stryker-plugins";
48
- //#region src/output-mode.ts
49
- /**
50
- * The known tool variables. Narrow per the plan — exactly
51
- * `['CLAUDECODE', 'CODEX_SANDBOX']` and load-bearing rather than a
52
- * fallback: they cover the PTY-allocating harnesses a stdin condition would
53
- * have rescued.
54
- */
55
- const TOOL_VARIABLES = ["CLAUDECODE", "CODEX_SANDBOX"];
56
- /**
57
- * Resolves the output mode by R4 precedence. Pure — reads nothing, so it is
58
- * fully testable; the caller supplies every input once at startup. The
59
- * mutually-exclusive-flags case is a caller error, returned as a `failure` so
60
- * the function stays total.
61
- */
62
- function resolveMode(input) {
63
- if (input.text === true && input.json === true) return Result.fail(CliError.InvalidValue.make({
64
- option: "json",
65
- value: "text",
66
- expected: "the \"--format text\" and \"--json\" flags are mutually exclusive — use one or the other",
67
- kind: "flag"
68
- }));
69
- if (input.text === true) return Result.succeed({
70
- mode: "human",
71
- signal: "flag",
72
- stdoutIsTTY: input.stdoutIsTTY
73
- });
74
- if (input.json === true) return Result.succeed({
75
- mode: "machine",
76
- signal: "flag",
77
- stdoutIsTTY: input.stdoutIsTTY
78
- });
79
- if (input.envMode !== void 0 && input.envMode.length > 0) return Result.succeed({
80
- mode: input.envMode === "machine" ? "machine" : "human",
81
- signal: "env",
82
- stdoutIsTTY: input.stdoutIsTTY
83
- });
84
- if (!input.stdoutIsTTY) return Result.succeed({
85
- mode: "machine",
86
- signal: "tty",
87
- stdoutIsTTY: false
88
- });
89
- if (input.agent !== void 0 && input.agent.length > 0) return Result.succeed({
90
- mode: "machine",
91
- signal: "agent",
92
- stdoutIsTTY: true
43
+ import * as Stdio from "effect/Stdio";
44
+ import * as Stream from "effect/Stream";
45
+ import { sha256 } from "@noble/hashes/sha256";
46
+ import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
47
+ //#region src/RunOutcome.workflow.ts
48
+ var RunOutcomeCommand = class extends S.TaggedClass()("RunOutcomeCommand", {
49
+ succeeded: S.Boolean,
50
+ signal: S.optional(S.Finite),
51
+ interrupted: S.Boolean,
52
+ helpErrorCount: S.optional(S.Finite),
53
+ cliError: S.Boolean,
54
+ unrecognized: S.optional(S.String),
55
+ survivorsReason: S.optional(S.Literals(["no-report", "mismatch"])),
56
+ survivorsDiagnostic: S.optional(S.String),
57
+ schemaError: S.Boolean,
58
+ successExitClass: S.optional(ExitClass),
59
+ highestExitClass: S.optional(ExitClass),
60
+ configDetail: S.optional(S.String),
61
+ diagnostic: S.optional(S.String)
62
+ }) {};
63
+ const CONFIG_CODE$1 = 2;
64
+ 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);
65
+ var RunOk = class extends S.TaggedClass()("RunOk", { help: S.Boolean }) {};
66
+ var RunInterrupted = class extends S.TaggedError()("RunInterrupted", { code: S.Finite }) {};
67
+ var RunParseFailed = class extends S.TaggedError()("RunParseFailed", { unrecognized: S.optional(S.String) }) {};
68
+ var RunSurvivorsRejected = class extends S.TaggedError()("RunSurvivorsRejected", {
69
+ reason: S.Literals(["no-report", "mismatch"]),
70
+ diagnostic: S.optional(S.String)
71
+ }) {};
72
+ var RunConfigFailed = class extends S.TaggedError()("RunConfigFailed", { detail: S.optional(S.String) }) {};
73
+ var RunFailed$1 = class extends S.TaggedError()("RunFailed", {
74
+ code: S.Finite,
75
+ diagnostic: S.optional(S.String)
76
+ }) {};
77
+ function classify(command) {
78
+ if (command.signal !== void 0) return RunInterrupted.make({ code: 128 + command.signal });
79
+ if (command.succeeded) {
80
+ if (command.successExitClass !== void 0) return RunFailed$1.make({
81
+ code: classCode(command.successExitClass),
82
+ diagnostic: command.diagnostic
83
+ });
84
+ return RunOk.make({ help: false });
85
+ }
86
+ if (command.interrupted) return RunInterrupted.make({ code: 1 });
87
+ if (command.helpErrorCount !== void 0) {
88
+ if (command.helpErrorCount > 0) return RunParseFailed.make({ unrecognized: command.unrecognized });
89
+ return RunOk.make({ help: true });
90
+ }
91
+ if (command.cliError) return RunParseFailed.make({ unrecognized: command.unrecognized });
92
+ if (command.survivorsReason !== void 0) return RunSurvivorsRejected.make({
93
+ reason: command.survivorsReason,
94
+ diagnostic: command.survivorsDiagnostic
93
95
  });
94
- for (const variable of TOOL_VARIABLES) {
95
- const value = input.toolVars?.[variable];
96
- if (value !== void 0 && value.length > 0) return Result.succeed({
97
- mode: "machine",
98
- signal: "tool",
99
- stdoutIsTTY: true
96
+ if (command.schemaError) return RunConfigFailed.make({ detail: command.configDetail });
97
+ if (command.highestExitClass !== void 0) {
98
+ if (command.highestExitClass === "ConfigError") return RunConfigFailed.make({ detail: command.configDetail });
99
+ return RunFailed$1.make({
100
+ code: classCode(command.highestExitClass),
101
+ diagnostic: command.diagnostic
100
102
  });
101
103
  }
102
- return Result.succeed({
103
- mode: "human",
104
- signal: "tty",
105
- stdoutIsTTY: true
104
+ return RunFailed$1.make({
105
+ code: 1,
106
+ diagnostic: command.diagnostic
106
107
  });
107
108
  }
108
- /**
109
- * The log colouriser's gate (R8). Machine mode never emits colour, so a
110
- * harness merging `2>&1` is not handed escape sequences it must strip, and
111
- * `NO_COLOR` is honoured for the human path per the convention: any value
112
- * other than an unset or empty variable disables colour.
113
- */
114
- function isColorEnabled(resolved, noColor) {
115
- return resolved.mode === "human" && (noColor === void 0 || noColor.length === 0);
109
+ function succeedRun(ok) {
110
+ return Result.succeed(ok);
116
111
  }
117
- //#endregion
118
- //#region src/output-mode-probe.ts
119
- var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/output-mode-probe/OutputModeProbeTag") {};
120
- const OutputModeProbe = OutputModeProbeTag;
121
- const OutputModeProbeLive = Layer.succeed(OutputModeProbe, OutputModeProbe.of({ detectMode: () => {
122
- const envMode = process.env["STRYKER_MODE"];
123
- const agent = process.env["AGENT"];
124
- return Result.getOrThrow(resolveMode({
125
- stdoutIsTTY: process.stdout.isTTY === true,
126
- ...envMode !== void 0 ? { envMode } : {},
127
- ...agent !== void 0 ? { agent } : {},
128
- toolVars: Object.fromEntries(TOOL_VARIABLES.map((variable) => [variable, process.env[variable]]))
129
- }));
130
- } }));
131
- //#endregion
132
- //#region src/stream-protocol.ts
133
- /**
134
- * The heartbeat interval (R19), matching Terraform's `apply_progress`
135
- * cadence: long enough that a slow phase is not noisy, short enough that a
136
- * consumer can tell "slow" from "hung" without waiting for a mutant event.
137
- */
138
- const TICK_INTERVAL_MS = 1e4;
139
- //#endregion
140
- //#region src/run-event-stream.ts
141
- var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/run-event-stream/RunEventStreamPortTag") {};
142
- const RunEventStreamPort = RunEventStreamPortTag;
143
- const isTerminalEvent = (event) => event.kind === "verdict" || event.kind === "error" || event.kind === "help" || event.kind === "manifest";
144
- /**
145
- * The push adapter from the run's synchronous sink to the callback mailbox.
146
- * `Queue.offer`/`Queue.end` on an unbounded queue never block or fail, so the
147
- * sync sink can drive them with `Effect.runSyncWith(ctx)` using the context
148
- * captured where the sink is constructed. Takes the queue and the captured
149
- * context, keeping the adapter pure with respect to its environment.
150
- */
151
- function queueEmit(queue, ctx) {
152
- return {
153
- single: (event) => {
154
- Effect.runSyncWith(ctx)(Queue.offer(queue, event));
155
- },
156
- end: () => {
157
- Effect.runSyncWith(ctx)(Queue.end(queue));
158
- }
159
- };
112
+ function failRun(error) {
113
+ return Result.fail(error);
160
114
  }
161
- /**
162
- * The drain writes the framed lines through the platform's `Stdio` stdout
163
- * sink — `NodeStdio.layer` at the composition root supplies the service, and
164
- * the sink owns the writable's backpressure, the scoped `'error'` listener a
165
- * closed consumer raises, and the final `'finish'` wait (`endOnDone`). A
166
- * write failure surfaces as a `PlatformError` which this catch swallows — a
167
- * consumer closing the pipe must not replace the run's classed exit code
168
- * (R31) — and the drain still completes only after every byte was handed to
169
- * the OS (R30).
170
- */
171
- const drainOf = (stdio, framed) => Stream.run(framed, stdio.stdout({ endOnDone: true })).pipe(Effect.ignore);
172
- /**
173
- * Creates a run's stream. The drain is an Effect the composition root forks
174
- * before the run; until then the sink is unbound and every push is dropped,
175
- * which is what makes human mode — or an absent drain — inert without
176
- * per-call probing (R2). The run's clock zero is read from the runtime so
177
- * the adapter never touches the wall clock directly.
178
- */
179
- const makeRunEventStream = (stdio, resolved) => Effect.gen(function* () {
180
- const runId = generateRunId();
181
- const startedAt = yield* Clock.currentTimeMillis;
182
- const ctx = yield* Effect.context();
183
- const state = {
184
- mode: resolved.mode,
185
- signal: resolved.signal,
186
- emit: null,
187
- headerWritten: false,
188
- terminalWritten: false,
189
- progress: {
190
- completed: 0,
191
- total: null
192
- }
193
- };
194
- const registered = yield* Deferred.make();
195
- const eventStream = Stream.callback((queue) => Effect.sync(() => {
196
- state.emit = queueEmit(queue, ctx);
197
- }).pipe(Effect.andThen(Deferred.succeed(registered, void 0))));
198
- const tickStream = Stream.tick(TICK_INTERVAL_MS).pipe(Stream.filter(() => state.mode === "machine" && state.headerWritten && !state.terminalWritten), Stream.mapEffect(() => Effect.gen(function* () {
199
- return {
200
- kind: "tick",
201
- elapsedMs: (yield* Clock.currentTimeMillis) - startedAt,
202
- completed: state.progress.completed,
203
- total: state.progress.total
204
- };
205
- })));
206
- let terminalSeen = false;
207
- const framed = Stream.merge(eventStream, tickStream, { haltStrategy: "either" }).pipe(Stream.filter((event) => {
208
- if (terminalSeen) return false;
209
- if (isTerminalEvent(event)) terminalSeen = true;
210
- return true;
211
- }), Stream.map((event) => `${JSON.stringify(event)}\n`));
212
- const drain = drainOf(stdio, framed);
213
- let drainFiber = null;
214
- const sink = (event) => {
215
- const emit = state.emit;
216
- if (emit === null || state.terminalWritten || state.mode !== "machine") return;
217
- if (!state.headerWritten) {
218
- state.headerWritten = true;
219
- emit.single({
220
- kind: "stream",
221
- schemaVersion: "1.0",
222
- runId,
223
- mode: state.mode,
224
- signal: state.signal
225
- });
226
- }
227
- switch (event.kind) {
228
- case "stream":
229
- case "tick": return;
230
- case "phase": break;
231
- case "plan":
232
- state.progress = {
233
- ...state.progress,
234
- total: event.total
235
- };
236
- break;
237
- case "mutant":
238
- state.progress = {
239
- completed: event.completed,
240
- total: event.total
241
- };
242
- break;
243
- case "verdict":
244
- case "error":
245
- case "help":
246
- case "manifest":
247
- emit.single(event);
248
- state.terminalWritten = true;
249
- emit.end();
250
- return;
251
- }
252
- emit.single(event);
253
- };
254
- return {
255
- sink,
256
- runId,
257
- startedAt,
258
- isOpen: () => state.emit !== null && state.mode === "machine" && !state.terminalWritten,
259
- ensureOpen: (openResolved) => {
260
- if (state.headerWritten) return;
261
- state.mode = openResolved.mode;
262
- state.signal = openResolved.signal;
263
- },
264
- open: Effect.gen(function* () {
265
- if (drainFiber === null) {
266
- drainFiber = yield* Effect.forkDetach(drain);
267
- yield* Effect.race(Deferred.await(registered), Fiber.await(drainFiber));
268
- }
269
- }),
270
- closeAndDrain: Effect.gen(function* () {
271
- state.terminalWritten = true;
272
- state.emit?.end();
273
- if (drainFiber !== null) yield* Fiber.join(drainFiber);
274
- })
275
- };
276
- });
277
- const RunEventStreamLive = Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
278
- //#endregion
279
- //#region src/signal-observer.ts
280
- const SIGNAL_NUMBERS = Object.freeze({
281
- SIGINT: 2,
282
- SIGTERM: 15
283
- });
284
- /**
285
- * Installs the listeners and returns the reader.
286
- *
287
- * The listener records and returns: interrupting the run is the runtime's job,
288
- * and doing it from here would race the run's own finalizer for the stream.
289
- * `once` per signal, because a second delivery of the same signal cannot
290
- * change the answer.
291
- */
292
- function observeTerminatingSignal() {
293
- let observed = null;
294
- for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
295
- observed = SIGNAL_NUMBERS[signal] ?? null;
296
- });
297
- return () => observed;
115
+ function runOutcomeDecision(command) {
116
+ 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);
298
117
  }
118
+ const runOutcomeWorkflow = Workflow.make(RunOutcomeCommand, runOutcomeDecision);
299
119
  //#endregion
300
- //#region src/survivors-report.schema.ts
120
+ //#region src/Survivors.workflow.ts
301
121
  /**
302
122
  * The mutant shape the admission carries, named once because both the decision's
303
123
  * `Admitted` payload and the command's precomputed survivor list are the same shape.
@@ -318,15 +138,6 @@ const MutantShape = S.Struct({
318
138
  })
319
139
  })
320
140
  });
321
- /**
322
- * The prior report as a document, decoded at the boundary. Module-internal: consumers
323
- * get the decode function, not the schema, so the report's wire shape is not a
324
- * surface commitment and the codec has exactly one caller.
325
- *
326
- * `status` is a bare string rather than the closed status set on purpose: the decide only
327
- * compares it to `'Survived'`, so a report written by a newer engine that added a status
328
- * must not be refused for carrying one.
329
- */
330
141
  const PriorReportDocument = S.Struct({
331
142
  config: S.optional(S.Record(S.String, S.Unknown)),
332
143
  framework: S.optional(S.Struct({ version: S.optional(S.String) })),
@@ -350,8 +161,6 @@ const PriorReportDocument = S.Struct({
350
161
  }))
351
162
  }))
352
163
  });
353
- //#endregion
354
- //#region src/survivors-admission.workflow.ts
355
164
  /**
356
165
  * U8 — survivor re-run admission (R10, R11, KTD6, KTD7).
357
166
  *
@@ -475,22 +284,6 @@ function hashesMatch(priorReport, input) {
475
284
  sourceContentHashes: input.sourceContentHashes
476
285
  });
477
286
  }
478
- const rejection = (reason, detail) => ({
479
- kind: "reject",
480
- reason,
481
- remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
482
- });
483
- function admissionVerdict(input) {
484
- const priorReport = input.priorReport;
485
- if (priorReport === void 0) return rejection("no-report", NO_REPORT_DETAIL);
486
- if (wasProducedBySurvivorsRun(priorReport)) return rejection("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
487
- if (input.priorSurvivors.length === 0) return { kind: "no-survivors" };
488
- if (!hashesMatch(priorReport, input)) return rejection("mismatch", MISMATCH_DETAIL);
489
- return {
490
- kind: "admit",
491
- survivors: input.priorSurvivors
492
- };
493
- }
494
287
  const SurvivorsAdmissionTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/SurvivorsAdmission");
495
288
  var Admitted = class extends S.TaggedClass()("Admitted", { survivors: S.Array(MutantShape) }) {
496
289
  [SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
@@ -505,48 +298,50 @@ var SurvivorsRejection = class extends S.TaggedError()("SurvivorsRejection", {
505
298
  }) {
506
299
  [SurvivorsAdmissionTypeId] = SurvivorsAdmissionTypeId;
507
300
  };
508
- /**
509
- * The survivors admission decision: the classification `admissionVerdict`
510
- * produces, assigned to the workflow channels — one arm per kind, no guard
511
- * chain. A missing report, a survivors-sourced report and a hash mismatch are
512
- * the same reject outcome with different reasons; only the rejection's
513
- * remediation names the full run to do first (R10).
514
- */
515
- const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, (command) => Match.value(admissionVerdict(command)).pipe(Match.discriminator("kind")("reject", (verdict) => Result.fail(SurvivorsRejection.make({
516
- reason: verdict.reason,
517
- remediation: verdict.remediation
518
- }))), Match.discriminator("kind")("no-survivors", () => Result.succeed(NoSurvivors.make())), Match.discriminator("kind")("admit", (verdict) => Result.succeed(Admitted.make({ survivors: verdict.survivors }))), Match.exhaustive));
519
- //#endregion
520
- //#region src/survivors-exit.ts
521
- /** The exit class a rejected survivors run exits with (R6: exit 2). */
522
- const SURVIVORS_REJECT_EXIT_CLASS = ExitClass.ConfigError;
523
- //#endregion
524
- //#region src/cli-exit-code.ts
525
- function isExitClass(value) {
526
- return value === ExitClass.VerdictFail || value === ExitClass.ConfigError || value === ExitClass.RuntimeError || value === ExitClass.InternalError;
301
+ function reject(reason, detail) {
302
+ return Result.fail(SurvivorsRejection.make({
303
+ reason,
304
+ remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
305
+ }));
306
+ }
307
+ function decideAdmission(input) {
308
+ const priorReport = input.priorReport;
309
+ if (priorReport === void 0) return reject("no-report", NO_REPORT_DETAIL);
310
+ if (wasProducedBySurvivorsRun(priorReport)) return reject("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
311
+ if (input.priorSurvivors.length === 0) return Result.succeed(NoSurvivors.make());
312
+ if (!hashesMatch(priorReport, input)) return reject("mismatch", MISMATCH_DETAIL);
313
+ return Result.succeed(Admitted.make({ survivors: input.priorSurvivors }));
314
+ }
315
+ function admissionDecision(command) {
316
+ return decideAdmission(command);
527
317
  }
318
+ const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, admissionDecision);
319
+ //#endregion
320
+ //#region src/Envelope.ts
528
321
  /**
529
- * The `exitClass` of a tagged error, when it carries one.
322
+ * Envelope the failure envelope and console capture leaf.
530
323
  *
531
- * Every `*.schema.ts` error in `mutation-run` carries a `readonly exitClass`
532
- * member (or schema field) `2` for config, `3` for runtime, `4` for
533
- * internal. The field is deliberately off-wire, but at the CLI edge the
534
- * errors are in-process objects, so it is readable via `Reflect.get`.
324
+ * Extracted from Cli.ts to break the import cycle Cli <-> Output.
325
+ * Both Cli and Output import from this leaf, so neither depends on the other
326
+ * for these values. This file imports only from external packages and from
327
+ * StreamVersion (leaf) and Survivors.workflow (leaf), never from Cli or Output
328
+ * themselves.
535
329
  */
330
+ const CONFIG_CODE = 2;
331
+ function runOutcomeCode(result) {
332
+ if (Result.isSuccess(result)) return 0;
333
+ 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);
334
+ }
335
+ function isExitClass(value) {
336
+ return S.is(ExitClass)(value);
337
+ }
536
338
  function exitClassOf(value) {
537
339
  if (typeof value !== "object" || value === null) return;
538
340
  if (!("exitClass" in value)) return;
539
341
  const raw = Reflect.get(value, "exitClass");
540
- if (typeof raw !== "number" || !isExitClass(raw)) return;
342
+ if (!isExitClass(raw)) return;
541
343
  return raw;
542
344
  }
543
- /**
544
- * Collects every `exitClass` present in a value's nested `cause` chain.
545
- *
546
- * Depth-capped and cycle-safe: schema errors nest (`PrepareFailedError`
547
- * wrapping `ConfigFileInvalidError` wrapping a validation detail), and a
548
- * malformed chain must not recurse unboundedly.
549
- */
550
345
  function collectExitClassesFromValue(value, out, seen, depth) {
551
346
  if (depth > 10 || value === null || value === void 0) return;
552
347
  if (typeof value !== "object") return;
@@ -560,105 +355,37 @@ function collectExitClassesFromValue(value, out, seen, depth) {
560
355
  else collectExitClassesFromValue(causeVal, out, seen, depth + 1);
561
356
  }
562
357
  }
563
- /**
564
- * Collects every `exitClass` present in the failure's `Cause` and in each
565
- * error's nested `cause` field.
566
- *
567
- * A `Cause` holds typed `Fail` and defect `Die` reasons
568
- * (`repos/effect/packages/effect/src/Cause.ts:144-196`, `isFailReason` /
569
- * `isDieReason`). `Effect.mapError` replaces the error value while keeping
570
- * only the mapped error in `cause.reasons`
571
- * (`repos/effect/packages/effect/src/internal/effect.ts:3253-3267`), so a
572
- * wrapped config error is invisible at the top level and must be found by
573
- * walking both the `Cause` reasons and each error's own `cause` field.
574
- */
575
358
  function collectExitClasses(exit) {
576
359
  const out = [];
577
360
  const seen = /* @__PURE__ */ new WeakSet();
578
361
  if (Exit.isFailure(exit)) for (const reason of exit.cause.reasons) {
579
- const candidate = Cause.isFailReason(reason) ? reason.error : Cause.isDieReason(reason) ? reason.defect : void 0;
362
+ let candidate;
363
+ if (Cause.isFailReason(reason)) candidate = reason.error;
364
+ else if (Cause.isDieReason(reason)) candidate = reason.defect;
365
+ else candidate = void 0;
580
366
  if (candidate !== void 0) collectExitClassesFromValue(candidate, out, seen, 0);
581
367
  }
582
368
  return out;
583
369
  }
584
- /**
585
- * Classifies a failed run for the finalizer: usage/parse failures
586
- * (`CliError` — except a bare help request, which exits 0), rejected
587
- * survivors runs (`SurvivorsRejection`), an unreadable prior report
588
- * (`S.SchemaError`) all exit 2; otherwise the highest `exitClass` found by
589
- * walking the failure's `Cause` reasons and each error's nested `cause` field
590
- * wins by the precedence `4 > 3 > 2 > 1` (via `highestExitClass`); no class
591
- * found is 1 (the framework's default). A successful run exits 0; the verdict
592
- * gates (U5) then resolve the final classed code.
593
- *
594
- * The report parse failure shares the survivors class deliberately. It is not a
595
- * verdict — the decider never sees the report — but the operator's answer is the
596
- * same class of answer as a rejection: the input you named cannot be used. Letting
597
- * it fall through to 1 would make an unusable `--survivors` input indistinguishable
598
- * from a crash.
599
- *
600
- * Previously this matched only a top-level `ConfigError` via `carriesConfigError`,
601
- * so a `PrepareFailedError` wrapping a `ConfigFileInvalidError` was invisible
602
- * and classes 3 and 4 were unreachable. Walking the chain makes them reachable.
603
- */
604
- function resolveCliExitCode(exit) {
605
- if (Exit.isSuccess(exit)) return 0;
606
- if (Cause.hasInterruptsOnly(exit.cause)) return 1;
607
- const failure = Cause.findErrorOption(exit.cause);
608
- if (Option.isSome(failure)) {
609
- const value = failure.value;
610
- if (S.is(CliError.ShowHelp)(value)) return value.errors.length > 0 ? 2 : 0;
611
- if (CliError.isCliError(value)) return 2;
612
- if (S.is(SurvivorsRejection)(value)) return SURVIVORS_REJECT_EXIT_CLASS;
613
- if (S.isSchemaError(value)) return SURVIVORS_REJECT_EXIT_CLASS;
614
- }
615
- const classes = collectExitClasses(exit);
616
- const highest = highestExitClass(classes);
617
- if (highest !== null) return highest;
618
- return 1;
619
- }
620
- //#endregion
621
- //#region src/cli-failure-text.ts
622
- /**
623
- * The reason a domain error carries, when it carries one.
624
- *
625
- * Every stage error in this engine is an `S.TaggedError` whose payload field is
626
- * `reason` — `DryRunNoTestsError`, `DryRunFailedError`, `PrepareFailedError`
627
- * and friends. Those classes extend `Error`, but nothing assigns `.message`, so
628
- * reading `.message` off one yields the empty string and the operator is told
629
- * a run failed with no indication of why. Read the field the errors actually
630
- * populate, and fall back only when it is absent.
631
- */
632
370
  function reasonOf(value) {
633
371
  if (!("reason" in value)) return;
634
372
  const reason = Reflect.get(value, "reason");
635
373
  if (typeof reason !== "string" || reason.length === 0) return;
636
374
  const detail = causeTextOf(value);
637
- return detail === void 0 ? reason : `${reason}: ${detail}`;
375
+ if (detail === void 0) return reason;
376
+ return `${reason}: ${detail}`;
638
377
  }
639
- /**
640
- * The human-readable text of a domain error's wrapped `cause`, if it has one.
641
- *
642
- * Recurses, because these errors nest: a stage error wraps a
643
- * `TestRunnerFailed`, which wraps the spawn or import failure that actually
644
- * happened. Stopping at the first layer reports a tag name — "TestRunnerFailed"
645
- * — and leaves the operator to guess. Each layer contributes only what it
646
- * knows, so the reader gets the chain down to the real fault.
647
- */
648
378
  function causeTextOf(value, depth = 0) {
649
379
  if (depth > 4 || !("cause" in value)) return;
650
380
  const cause = Reflect.get(value, "cause");
651
381
  return causeText(cause, depth + 1);
652
382
  }
653
383
  function configDetailOf(value) {
654
- const reason = reasonOf(value);
655
- if (reason !== void 0) return reason;
656
- const text = causeText(value, 0);
657
- if (text !== void 0 && text.length > 0) return text;
658
- if ("message" in value) {
659
- const msg = Reflect.get(value, "message");
660
- if (typeof msg === "string" && msg.length > 0) return msg;
661
- }
384
+ if (!("reason" in value) && !("message" in value)) return;
385
+ const reason = Reflect.get(value, "reason");
386
+ if (typeof reason === "string" && reason.length > 0) return reason;
387
+ const message = Reflect.get(value, "message");
388
+ if (typeof message === "string" && message.length > 0) return message;
662
389
  }
663
390
  function shouldVisitConfigValue(value, depth, seen) {
664
391
  if (depth > 10) return false;
@@ -679,16 +406,15 @@ function pushConfigCauses(value, depth, stack) {
679
406
  depth: depth + 1
680
407
  });
681
408
  }
682
- /**
683
- * The first config-class error's detail in cause-chain order, for the
684
- * config remediation.
685
- */
686
409
  function firstConfigErrorDetail(exit) {
687
410
  if (!Exit.isFailure(exit)) return;
688
411
  const seen = /* @__PURE__ */ new WeakSet();
689
412
  const stack = [];
690
413
  for (const reason of exit.cause.reasons) {
691
- const candidate = Cause.isFailReason(reason) ? reason.error : Cause.isDieReason(reason) ? reason.defect : void 0;
414
+ let candidate;
415
+ if (Cause.isFailReason(reason)) candidate = reason.error;
416
+ else if (Cause.isDieReason(reason)) candidate = reason.defect;
417
+ else candidate = void 0;
692
418
  if (candidate !== void 0) stack.push({
693
419
  value: candidate,
694
420
  depth: 0
@@ -700,128 +426,195 @@ function firstConfigErrorDetail(exit) {
700
426
  const { value, depth } = entry;
701
427
  if (!shouldVisitConfigValue(value, depth, seen)) continue;
702
428
  seen.add(value);
703
- if (exitClassOf(value) === ExitClass.ConfigError) {
429
+ if (exitClassOf(value) === "ConfigError") {
704
430
  const detail = configDetailOf(value);
705
431
  if (detail !== void 0) return detail;
706
432
  }
707
433
  pushConfigCauses(value, depth, stack);
708
434
  }
709
435
  }
710
- /**
711
- * The contextual remediation for a failure, picked from the class of the
712
- * failure whose cause chain contains an `exitClass` of `ConfigError` (2)
713
- * names the config file, rejected survivors runs name the full run to do
714
- * first. Everything else points at the report file and the verdict envelope,
715
- * which is where a runtime failure's detail already is. The classification
716
- * walks the `Cause` reasons and each error's nested `cause` field — see
717
- * `collectExitClasses`.
718
- */
719
- function remediationFor(exit, code) {
720
- if (code > 128) return "the run was interrupted by a signal; re-run it to continue";
436
+ function describeFailure(exit) {
437
+ if (!Exit.isFailure(exit)) return "Unknown failure";
721
438
  const value = failureValue(exit);
722
439
  if (value !== void 0) {
723
- if (CliError.isCliError(value)) return "re-run with --help to see the full usage";
724
440
  if (S.is(SurvivorsRejection)(value)) return value.remediation;
441
+ if (typeof value === "object" && value !== null) {
442
+ const reason = reasonOf(value);
443
+ if (reason !== void 0) return reason;
444
+ }
445
+ if (value instanceof Error && value.message.length > 0) return value.message;
446
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
725
447
  }
726
- if (collectExitClasses(exit).includes(ExitClass.ConfigError)) {
448
+ if (collectExitClasses(exit).includes("ConfigError")) {
727
449
  const detail = firstConfigErrorDetail(exit);
728
- return detail !== void 0 ? `check the config file: ${detail}` : "check the config file";
450
+ if (detail !== void 0) return detail;
729
451
  }
730
- return "see --reportFile or the verdict envelope on stdout";
452
+ const rendered = Cause.pretty(exit.cause);
453
+ if (rendered.length > 0) return rendered;
454
+ return "Unknown failure";
731
455
  }
732
- /**
733
- * The failure's own text, used when the capture buffer is empty — a failure
734
- * stryker reported through its own logger rather than the framework's
735
- * `Console`. Falls back to a rendered cause.
736
- */
737
- function describeFailure(exit) {
738
- if (Exit.isFailure(exit)) {
739
- const value = failureValue(exit);
740
- if (value !== void 0) {
741
- if (S.is(SurvivorsRejection)(value)) return value.remediation;
742
- if (typeof value === "object" && value !== null) {
743
- const reason = reasonOf(value);
744
- if (reason !== void 0) return reason;
745
- }
746
- if (value instanceof Error && value.message.length > 0) return value.message;
747
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
748
- return Cause.pretty(exit.cause);
749
- }
750
- return Cause.pretty(exit.cause);
751
- }
752
- return "";
753
- }
754
- /**
755
- * The argument the framework reports it does not know, named the way the wire
756
- * contract spells it. The v4 parser fails wrapped in a ShowHelp whose errors
757
- * carry the offending flag or operand; when the unrecognized flag was given a
758
- * separate value (`--format text`), the value is the token the old parser
759
- * reported, so the token after the flag is named when one was given.
760
- */
761
456
  function unrecognizedArgumentOf(exit, argv) {
762
457
  if (!Exit.isFailure(exit)) return;
763
458
  const value = failureValue(exit);
764
459
  if (value === void 0 || !CliError.isCliError(value)) return;
765
- const errors = S.is(CliError.ShowHelp)(value) ? value.errors : [value];
460
+ const errors = (() => {
461
+ if (S.is(CliError.ShowHelp)(value)) return value.errors;
462
+ return [value];
463
+ })();
766
464
  for (const error of errors) {
767
465
  if (S.is(CliError.UnrecognizedOption)(error)) {
768
466
  const at = argv.indexOf(error.option);
769
- const next = at >= 0 ? argv[at + 1] : void 0;
770
- return next !== void 0 && !next.startsWith("-") ? next : error.option;
467
+ const next = (() => {
468
+ if (at >= 0) return argv[at + 1];
469
+ })();
470
+ if (next !== void 0 && !next.startsWith("-")) return next;
471
+ return error.option;
771
472
  }
772
473
  if (S.is(CliError.UnexpectedArgument)(error)) return error.arguments[0];
773
474
  if (S.is(CliError.UnknownSubcommand)(error)) return error.subcommand;
774
475
  }
775
476
  }
776
- /**
777
- * The first typed error in the exit's cause. The framework fails with
778
- * `Cause.fail` (usage errors); the run handler is `Effect.promise`, whose
779
- * rejected promises surface as *defects* (`Die` reasons) rather than
780
- * failures — so stryker's own ConfigError/StrykerError values arrive there
781
- * and must be read from the cause's `Die` reasons.
782
- */
783
477
  function failureValue(exit) {
784
478
  if (!Exit.isFailure(exit)) return;
785
479
  const failure = Cause.findErrorOption(exit.cause);
786
480
  if (Option.isSome(failure)) return failure.value;
787
- const dieReason = exit.cause.reasons.find(Cause.isDieReason);
788
- return dieReason === void 0 ? void 0 : dieReason.defect;
789
481
  }
790
- //#endregion
791
- //#region src/cli-error-envelope.ts
792
- function buildErrorEnvelope(exit, code, captured, argv) {
793
- const unrecognized = unrecognizedArgumentOf(exit, argv);
482
+ const SIGNAL_REMEDIATION = "the run was interrupted by a signal; re-run it to continue";
483
+ const PARSE_REMEDIATION = "re-run with --help to see the full usage";
484
+ const DEFAULT_REMEDIATION = "see --reportFile or the verdict envelope on stdout";
485
+ function successExitClassOf(exit) {
486
+ if (!Exit.isSuccess(exit)) return;
487
+ const value = exit.value;
488
+ if (!Predicate.hasProperty(value, "verdict")) return;
489
+ const candidate = value.verdict;
490
+ if (!isExitClass(candidate)) return;
491
+ return candidate;
492
+ }
493
+ function helpErrorCountOf(value) {
494
+ if (value === void 0) return;
495
+ if (!S.is(CliError.ShowHelp)(value)) return;
496
+ return value.errors.length;
497
+ }
498
+ function survivorsRejectionOf(value) {
499
+ if (value === void 0) return;
500
+ if (!S.is(SurvivorsRejection)(value)) return;
501
+ return value;
502
+ }
503
+ function present(value) {
504
+ if (value === null) return;
505
+ return value;
506
+ }
507
+ function survivorsReasonOf(survivors) {
508
+ if (survivors === void 0) return;
509
+ return survivors.reason;
510
+ }
511
+ function survivorsDiagnosticOf(survivors) {
512
+ if (survivors === void 0) return;
513
+ return survivors.remediation;
514
+ }
515
+ function omitUnknownFailure(diagnostic) {
516
+ if (diagnostic === "Unknown failure") return;
517
+ return diagnostic;
518
+ }
519
+ function capturedOrUnknown(captured) {
520
+ if (captured.length > 0) return captured;
521
+ return "Unknown failure";
522
+ }
523
+ function gatherRunOutcome(exit, signal, argv) {
524
+ const value = failureValue(exit);
525
+ const survivors = survivorsRejectionOf(value);
526
+ return RunOutcomeCommand.make({
527
+ succeeded: Exit.isSuccess(exit),
528
+ signal: present(signal),
529
+ interrupted: Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause),
530
+ helpErrorCount: helpErrorCountOf(value),
531
+ cliError: value !== void 0 && CliError.isCliError(value),
532
+ unrecognized: unrecognizedArgumentOf(exit, argv),
533
+ survivorsReason: survivorsReasonOf(survivors),
534
+ survivorsDiagnostic: survivorsDiagnosticOf(survivors),
535
+ schemaError: value !== void 0 && S.isSchemaError(value),
536
+ successExitClass: successExitClassOf(exit),
537
+ highestExitClass: present(highestExitClass(collectExitClasses(exit))),
538
+ configDetail: firstConfigErrorDetail(exit),
539
+ diagnostic: omitUnknownFailure(describeFailure(exit))
540
+ });
541
+ }
542
+ function errorText(error, captured) {
543
+ return Match.value(error).pipe(Match.tag("RunParseFailed", (failed) => {
544
+ if (failed.unrecognized !== void 0) return `Received unknown argument: '${failed.unrecognized}'`;
545
+ return capturedOrUnknown(captured);
546
+ }), Match.tag("RunSurvivorsRejected", (failed) => {
547
+ if (failed.diagnostic !== void 0) return failed.diagnostic;
548
+ return "Unknown failure";
549
+ }), Match.tag("RunInterrupted", () => capturedOrUnknown(captured)), Match.tag("RunConfigFailed", (failed) => {
550
+ if (captured.length > 0) return captured;
551
+ if (failed.detail !== void 0) return failed.detail;
552
+ return "Unknown failure";
553
+ }), Match.tag("RunFailed", (failed) => {
554
+ if (captured.length > 0) return captured;
555
+ if (failed.diagnostic !== void 0) return failed.diagnostic;
556
+ return "Unknown failure";
557
+ }), Match.exhaustive);
558
+ }
559
+ function remediationText(error) {
560
+ return Match.value(error).pipe(Match.tag("RunInterrupted", (failed) => {
561
+ if (failed.code > 128) return SIGNAL_REMEDIATION;
562
+ return DEFAULT_REMEDIATION;
563
+ }), Match.tag("RunParseFailed", () => PARSE_REMEDIATION), Match.tag("RunSurvivorsRejected", (failed) => {
564
+ if (failed.diagnostic !== void 0) return failed.diagnostic;
565
+ return DEFAULT_REMEDIATION;
566
+ }), Match.tag("RunConfigFailed", (failed) => {
567
+ if (failed.detail !== void 0) return `check the config file: ${failed.detail}`;
568
+ return "check the config file";
569
+ }), Match.tag("RunFailed", () => DEFAULT_REMEDIATION), Match.exhaustive);
570
+ }
571
+ function shapeEnvelope(error, captured) {
794
572
  return {
795
573
  schemaVersion: "1.0",
796
- code,
797
- error: unrecognized !== void 0 ? `Received unknown argument: '${unrecognized}'` : captured.length > 0 ? captured : describeFailure(exit),
798
- remediation: remediationFor(exit, code)
574
+ code: runOutcomeCode(Result.fail(error)),
575
+ error: errorText(error, captured),
576
+ remediation: remediationText(error)
799
577
  };
800
578
  }
801
- //#endregion
802
- //#region src/console-capture.ts
803
- /**
804
- * U6 — the machine-mode `Console` layer (KTD3, R7).
805
- *
806
- * The v4 CLI renders help and errors through an ANSI renderer and prints them
807
- * with the `Console` reference — and there is no seam to intercept: the
808
- * document is written *before* the failure propagates. Machine mode therefore
809
- * replaces the `Console` reference itself with a capturing implementation:
810
- * every write lands in an in-memory buffer instead of the real stdout/stderr,
811
- * and the terminating bootstrap (StrykerCliHandler.ts) emits the buffer as
812
- * one JSON envelope at teardown. Human mode keeps the default console so the
813
- * framework's prose rendering is untouched.
814
- *
815
- * The v4 `Console.Console` interface is the sync `globalThis.console` shape
816
- * (the v3 service's effect-returning surface became the module-level
817
- * wrapper functions), so the capture implementation needs no `unsafe`
818
- * mirror — every method stores into the buffer directly.
819
- */
579
+ function classifyRunOutcome(exit, signal, argv) {
580
+ return runOutcomeWorkflow(gatherRunOutcome(exit, signal, argv));
581
+ }
820
582
  const capturedConsoleChunks = [];
821
583
  const countByLabel = /* @__PURE__ */ new Map();
822
584
  const timeByLabel = /* @__PURE__ */ new Map();
585
+ function inspectValue(value) {
586
+ if (typeof value === "string") return value;
587
+ return Formatter.format(value);
588
+ }
823
589
  function formatArgs(args) {
824
- return format(...args);
590
+ if (args.length === 0) return "";
591
+ const first = args[0];
592
+ if (typeof first === "string") {
593
+ let index = 1;
594
+ let result = first.replace(/%[sdijfopO%]/g, (match) => {
595
+ if (match === "%%") return "%";
596
+ if (index >= args.length) return match;
597
+ const arg = args[index++];
598
+ switch (match) {
599
+ case "%s": return String(arg);
600
+ case "%d":
601
+ case "%i":
602
+ case "%f": return Number(arg).toString();
603
+ case "%j": try {
604
+ return String(JSON.stringify(arg));
605
+ } catch {
606
+ return "[Circular]";
607
+ }
608
+ case "%o":
609
+ case "%O":
610
+ case "%p": return inspectValue(arg);
611
+ default: return match;
612
+ }
613
+ });
614
+ for (; index < args.length; index++) result += ` ${inspectValue(args[index])}`;
615
+ return result;
616
+ }
617
+ return args.map(inspectValue).join(" ");
825
618
  }
826
619
  function captureSync(args) {
827
620
  capturedConsoleChunks.push(formatArgs(args));
@@ -835,86 +628,352 @@ function captureCount(label) {
835
628
  countByLabel.set(key, next);
836
629
  capturedConsoleChunks.push(`${key}: ${next}`);
837
630
  }
838
- function captureTimeEnd(label, now) {
631
+ function captureTimeEnd(label, nowNanos) {
839
632
  const key = label ?? "default";
840
633
  const started = timeByLabel.get(key);
841
634
  if (started !== void 0) {
842
635
  timeByLabel.delete(key);
843
- capturedConsoleChunks.push(`${key}: ${now - started}ms`);
636
+ const diffMs = Number(nowNanos - started) / 1e6;
637
+ capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
844
638
  }
845
639
  }
846
640
  function captureTrace(args) {
847
641
  capturedConsoleChunks.push(`Trace: ${formatArgs(args)}\n${(/* @__PURE__ */ new Error()).stack ?? ""}`);
848
642
  }
849
- const capturingConsole = {
643
+ const makeCapturingConsole = (clock) => ({
850
644
  assert: (condition, ...args) => captureAssert(condition, args),
851
645
  clear: () => {},
852
646
  count: (label) => captureCount(label),
853
647
  countReset: (label) => countByLabel.delete(label ?? "default"),
854
648
  debug: (...args) => captureSync(args),
855
- dir: (item, options) => capturedConsoleChunks.push(inspect(item, options)),
856
- dirxml: (item) => capturedConsoleChunks.push(inspect(item)),
649
+ dir: (item, _options) => capturedConsoleChunks.push(Formatter.format(item)),
650
+ dirxml: (item) => capturedConsoleChunks.push(Formatter.format(item)),
857
651
  error: (...args) => captureSync(args),
858
652
  group: () => {},
859
653
  groupCollapsed: () => {},
860
654
  groupEnd: () => {},
861
655
  info: (...args) => captureSync(args),
862
656
  log: (...args) => captureSync(args),
863
- table: (tabularData) => capturedConsoleChunks.push(inspect(tabularData, {
864
- colors: false,
865
- depth: null
866
- })),
867
- time: (label) => timeByLabel.set(label ?? "default", performance.now()),
868
- timeEnd: (label) => captureTimeEnd(label, performance.now()),
657
+ table: (tabularData) => capturedConsoleChunks.push(Formatter.format(tabularData)),
658
+ time: (label) => timeByLabel.set(label ?? "default", clock.monotonicTimeNanosUnsafe()),
659
+ timeEnd: (label) => captureTimeEnd(label, clock.monotonicTimeNanosUnsafe()),
869
660
  timeLog: (label, ...args) => {
870
661
  const key = label ?? "default";
871
662
  const started = timeByLabel.get(key);
872
- if (started !== void 0) capturedConsoleChunks.push(`${key}: ${performance.now() - started}ms ${formatArgs(args)}`);
663
+ if (started === void 0) return;
664
+ const diffMs = Number(clock.monotonicTimeNanosUnsafe() - started) / 1e6;
665
+ if (args.length === 0) {
666
+ capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
667
+ return;
668
+ }
669
+ capturedConsoleChunks.push(`${key}: ${diffMs}ms ${formatArgs(args)}`);
873
670
  },
874
671
  trace: (...args) => captureTrace(args),
875
672
  warn: (...args) => captureSync(args)
876
- };
673
+ });
674
+ const machineConsoleLayer = Layer.effect(Console.Console, Clock.clockWith((clock) => Effect.sync(() => {
675
+ resetCapturedConsole();
676
+ return makeCapturingConsole(clock);
677
+ })));
678
+ function readCapturedConsole() {
679
+ return capturedConsoleChunks.join("\n");
680
+ }
681
+ function resetCapturedConsole() {
682
+ capturedConsoleChunks.length = 0;
683
+ countByLabel.clear();
684
+ timeByLabel.clear();
685
+ }
686
+ //#endregion
687
+ //#region src/Output.workflow.ts
688
+ const TOOL_VARIABLES$1 = ["CLAUDECODE", "CODEX_SANDBOX"];
877
689
  /**
878
- * The machine-mode `Console` layer. Building it clears the capture buffer so
879
- * every run starts empty; the terminating bootstrap reads the buffer back
880
- * through `readCapturedConsole` at teardown. A `Layer` is already lazy, so
881
- * the layer is a value: the reset effect runs when the layer is built.
882
- *
883
- * The layer must replace the `Console` reference the module-level
884
- * `Console.log`/`Console.error` wrappers read through their fiber context;
885
- * v4 reads the override the same way it reads any provided service, so a
886
- * plain provide is sufficient — no special `setConsole`-style primitive
887
- * exists any more. The reference's identifier is `never` (it carries no
888
- * requirement), so the layer's type is too.
889
- *
890
- * Human mode provides no Console binding at all: effect's own default
891
- * console delegates every method to the global console, which is exactly the
892
- * prose rendering a human-mode run uses. Mirroring it here would reimplement
893
- * the library default (V.7).
690
+ * The command of the output-mode workflow: a schema class, because `Workflow.make`
691
+ * derives the command type from it and pins the error channel at the construction site.
894
692
  */
895
- const machineConsoleLayer = Layer.effect(Console.Console, Effect.sync(() => {
896
- resetCapturedConsole();
897
- return capturingConsole;
898
- }));
693
+ var ResolveModeCommand = class extends S.TaggedClass()("ResolveModeCommand", {
694
+ stdoutIsTTY: S.Boolean,
695
+ text: S.optional(S.Boolean),
696
+ json: S.optional(S.Boolean),
697
+ envMode: S.optional(S.String),
698
+ agent: S.optional(S.String),
699
+ toolVars: S.optional(S.Record(S.String, S.String))
700
+ }) {};
899
701
  /**
900
- * The text captured so far by the machine console layer, joined into one
901
- * document the way a terminal would have rendered it (one console call per
902
- * line). Empty in human mode.
702
+ * The conflict error for mutually exclusive format flags. Defined locally so the
703
+ * workflow decision remains pure the sealed effect surface does not include
704
+ * `effect/unstable/cli/CliError`, so the decision returns this local error and
705
+ * the shell maps it to `CliError.InvalidValue` at the boundary.
903
706
  */
904
- function readCapturedConsole() {
905
- return capturedConsoleChunks.join("\n");
707
+ var ModeConflictError = class extends S.TaggedError()("ModeConflictError", {
708
+ option: S.String,
709
+ value: S.String,
710
+ expected: S.String
711
+ }) {};
712
+ const CONFLICT_EXPECTED = "the \"--format text\" and \"--json\" flags are mutually exclusive — use one or the other";
713
+ function r4(command) {
714
+ if (command.text === true && command.json === true) return Result.fail(ModeConflictError.make({
715
+ option: "json",
716
+ value: "text",
717
+ expected: CONFLICT_EXPECTED
718
+ }));
719
+ if (command.text === true) return Result.succeed({
720
+ mode: "human",
721
+ signal: "flag",
722
+ stdoutIsTTY: command.stdoutIsTTY
723
+ });
724
+ if (command.json === true) return Result.succeed({
725
+ mode: "machine",
726
+ signal: "flag",
727
+ stdoutIsTTY: command.stdoutIsTTY
728
+ });
729
+ if (command.envMode !== void 0 && command.envMode.length > 0) {
730
+ if (command.envMode === "machine") return Result.succeed({
731
+ mode: "machine",
732
+ signal: "env",
733
+ stdoutIsTTY: command.stdoutIsTTY
734
+ });
735
+ return Result.succeed({
736
+ mode: "human",
737
+ signal: "env",
738
+ stdoutIsTTY: command.stdoutIsTTY
739
+ });
740
+ }
741
+ if (!command.stdoutIsTTY) return Result.succeed({
742
+ mode: "machine",
743
+ signal: "tty",
744
+ stdoutIsTTY: false
745
+ });
746
+ if (command.agent !== void 0 && command.agent.length > 0) return Result.succeed({
747
+ mode: "machine",
748
+ signal: "agent",
749
+ stdoutIsTTY: true
750
+ });
751
+ const toolVars = command.toolVars ?? {};
752
+ for (const variable of TOOL_VARIABLES$1) {
753
+ const value = toolVars[variable];
754
+ if (typeof value === "string" && value.length > 0) return Result.succeed({
755
+ mode: "machine",
756
+ signal: "tool",
757
+ stdoutIsTTY: true
758
+ });
759
+ }
760
+ return Result.succeed({
761
+ mode: "human",
762
+ signal: "tty",
763
+ stdoutIsTTY: true
764
+ });
765
+ }
766
+ function modeDecision(command) {
767
+ return r4(command);
906
768
  }
769
+ const resolveModeWorkflow = Workflow.make(ResolveModeCommand, modeDecision);
770
+ //#endregion
771
+ //#region src/Output.ts
772
+ /**
773
+ * Output — the machine/human output capability.
774
+ *
775
+ * The NDJSON run-event stream, wire protocol constants, mode resolution probes,
776
+ * and machine-mode terminal output. Pure mode resolution lives in
777
+ * Output.workflow.ts.
778
+ */
779
+ /**
780
+ * The heartbeat interval (R19), matching Terraform's `apply_progress`
781
+ * cadence: long enough that a slow phase is not noisy, short enough that a
782
+ * consumer can tell "slow" from "hung" without waiting for a mutant event.
783
+ */
784
+ const TICK_INTERVAL_MS = 1e4;
785
+ var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/RunEventStreamPortTag") {};
786
+ const RunEventStreamPort = RunEventStreamPortTag;
787
+ const isTerminalEvent = (event) => Match.value(event).pipe(Match.tag("verdict", () => true), Match.tag("error", () => true), Match.tag("help", () => true), Match.tag("manifest", () => true), Match.orElse(() => false));
788
+ const wireKind = (event) => Match.value(event).pipe(Match.tag("stream", () => "stream"), Match.tag("phase", () => "phase"), Match.tag("plan", () => "plan"), Match.tag("mutant", () => "mutant"), Match.tag("tick", () => "tick"), Match.tag("verdict", () => "verdict"), Match.tag("error", () => "error"), Match.tag("help", () => "help"), Match.tag("manifest", () => "manifest"), Match.exhaustive);
789
+ const toWireLine = (event) => {
790
+ const fields = Object.fromEntries(Object.entries(event).filter(([key]) => key !== "_tag"));
791
+ return JSON.stringify({
792
+ kind: wireKind(event),
793
+ ...fields
794
+ });
795
+ };
796
+ function numberText(value, fallback) {
797
+ if (typeof value === "number") return String(value);
798
+ return fallback;
799
+ }
800
+ function phaseLine(phase) {
801
+ if (typeof phase === "string") return `phase ${phase}`;
802
+ return "phase ";
803
+ }
804
+ const stderrProgressLine = (event, alreadyClosed) => {
805
+ if (alreadyClosed) return;
806
+ return Match.value(event).pipe(Match.tag("plan", (e) => `plan ${numberText(e.total, "0")} mutants`), Match.tag("phase", (e) => phaseLine(e.phase)), Match.tag("tick", (e) => `${numberText(e.completed, "0")}/${numberText(e.total, "?")} elapsed ${numberText(e.elapsedMs, "0")}ms`), Match.tag("verdict", (e) => `score ${numberText(e.score, "n/a")} killed ${numberText(e.counts.killed, "0")} survived ${numberText(e.counts.survived, "0")}`), Match.tag("error", (e) => {
807
+ if (typeof e.error === "string") return `error ${e.error}`;
808
+ return "error ";
809
+ }), Match.orElse(() => void 0));
810
+ };
811
+ const writeStderr = (stdio, line) => Stream.run(Stream.succeed(`${line}\n`), stdio.stderr({ endOnDone: false })).pipe(Effect.ignore);
812
+ const drainOf = (stdio, framed) => Stream.run(framed, stdio.stdout({ endOnDone: true })).pipe(Effect.ignore);
813
+ const makeRunEventStream = (stdio, resolved, drainFramed = drainOf.bind(null, stdio)) => Effect.gen(function* () {
814
+ const runId = generateRunId();
815
+ const startedAt = yield* Clock.currentTimeMillis;
816
+ const queue = yield* Queue.unbounded();
817
+ const state = {
818
+ mode: resolved.mode,
819
+ signal: resolved.signal,
820
+ headerWritten: false,
821
+ terminalWritten: false,
822
+ progress: {
823
+ completed: 0,
824
+ total: null
825
+ },
826
+ findingsPrinted: 0
827
+ };
828
+ const queueStream = Stream.fromQueue(queue).pipe(Stream.tap((event) => Effect.sync(() => {
829
+ Match.value(event).pipe(Match.tag("plan", (e) => {
830
+ state.progress = {
831
+ ...state.progress,
832
+ total: e.total
833
+ };
834
+ }), Match.tag("mutant", (e) => {
835
+ state.progress = {
836
+ completed: e.completed,
837
+ total: e.total
838
+ };
839
+ }), Match.orElse(() => {}));
840
+ })));
841
+ const tickStream = Stream.tick(TICK_INTERVAL_MS).pipe(Stream.drop(1), Stream.filter(() => state.headerWritten && !state.terminalWritten), Stream.mapEffect(() => Effect.gen(function* () {
842
+ const now = yield* Clock.currentTimeMillis;
843
+ return Heartbeat.make({
844
+ elapsedMs: now - startedAt,
845
+ completed: state.progress.completed,
846
+ total: state.progress.total
847
+ });
848
+ })));
849
+ let terminalSeen = false;
850
+ const drain = drainFramed(Stream.merge(queueStream, tickStream, { haltStrategy: "either" }).pipe(Stream.tap((event) => {
851
+ const line = stderrProgressLine(event, state.terminalWritten);
852
+ if (isTerminalEvent(event)) state.terminalWritten = true;
853
+ if (line === void 0) return Effect.void;
854
+ return writeStderr(stdio, line);
855
+ })).pipe(Stream.filter((event) => {
856
+ if (state.mode !== "machine") return false;
857
+ if (terminalSeen) return false;
858
+ if (isTerminalEvent(event)) terminalSeen = true;
859
+ return true;
860
+ }), Stream.map((event) => `${toWireLine(event)}\n`)));
861
+ let drainFiber = null;
862
+ return {
863
+ queue,
864
+ runId,
865
+ startedAt,
866
+ isOpen: () => state.mode === "machine" && !state.terminalWritten && drainFiber !== null,
867
+ ensureOpen: (openResolved) => {
868
+ if (state.headerWritten) return;
869
+ state.mode = openResolved.mode;
870
+ state.signal = openResolved.signal;
871
+ },
872
+ open: Effect.gen(function* () {
873
+ if (drainFiber === null) {
874
+ if (!state.headerWritten) {
875
+ state.headerWritten = true;
876
+ if (state.mode === "machine") yield* Queue.offer(queue, RunStarted.make({
877
+ schemaVersion: "1.0",
878
+ runId,
879
+ mode: state.mode,
880
+ signal: state.signal
881
+ }));
882
+ }
883
+ drainFiber = yield* Effect.forkDetach(drain);
884
+ }
885
+ }),
886
+ closeAndDrain: Effect.gen(function* () {
887
+ state.terminalWritten = true;
888
+ yield* Queue.end(queue);
889
+ if (drainFiber !== null) yield* Fiber.join(drainFiber);
890
+ })
891
+ };
892
+ });
893
+ Layer.effect(RunEventStreamPort, Effect.map(Stdio.Stdio, (stdio) => RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved) })));
894
+ const TOOL_VARIABLES = ["CLAUDECODE", "CODEX_SANDBOX"];
907
895
  /**
908
- * Clears the capture buffer and the count/time tables. Called when the
909
- * machine layer is constructed so every run starts empty.
896
+ * The log colouriser's gate (R8). Machine mode never emits colour, so a
897
+ * harness merging `2>&1` is not handed escape sequences it must strip, and
898
+ * `NO_COLOR` is honoured for the human path per the convention: any value
899
+ * other than an unset or empty variable disables colour.
910
900
  */
911
- function resetCapturedConsole() {
912
- capturedConsoleChunks.length = 0;
913
- countByLabel.clear();
914
- timeByLabel.clear();
901
+ function isColorEnabled(resolved, noColor) {
902
+ if (resolved.mode !== "human") return false;
903
+ if (noColor === void 0) return true;
904
+ if (noColor.length === 0) return true;
905
+ return false;
915
906
  }
916
- //#endregion
917
- //#region src/cli-machine-output.ts
907
+ var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/OutputModeProbeTag") {};
908
+ const OutputModeProbe = OutputModeProbeTag;
909
+ const outputModeProbeDescription = pipe(Cell.read((command) => Effect.succeed((() => {
910
+ const toolVarsRecord = {};
911
+ for (const variable of TOOL_VARIABLES) {
912
+ const value = process.env[variable];
913
+ if (value !== void 0) toolVarsRecord[variable] = value;
914
+ }
915
+ const envMode = process.env["STRYKER_MODE"];
916
+ const agent = process.env["AGENT"];
917
+ let result = { stdoutIsTTY: process.stdout.isTTY === true };
918
+ if (Object.keys(toolVarsRecord).length > 0) result = {
919
+ ...result,
920
+ toolVars: toolVarsRecord
921
+ };
922
+ if (envMode !== void 0) result = {
923
+ ...result,
924
+ envMode
925
+ };
926
+ if (agent !== void 0) result = {
927
+ ...result,
928
+ agent
929
+ };
930
+ if (command.text !== void 0) result = {
931
+ ...result,
932
+ text: command.text
933
+ };
934
+ if (command.json !== void 0) result = {
935
+ ...result,
936
+ json: command.json
937
+ };
938
+ return result;
939
+ })())), Cell.decode((raw) => Result.succeed((() => {
940
+ const filteredToolVars = {};
941
+ if (raw.toolVars !== void 0) {
942
+ for (const [key, value] of Object.entries(raw.toolVars)) if (value !== void 0) filteredToolVars[key] = value;
943
+ }
944
+ let commandInput = { stdoutIsTTY: raw.stdoutIsTTY };
945
+ if (raw.text !== void 0) commandInput = {
946
+ ...commandInput,
947
+ text: raw.text
948
+ };
949
+ if (raw.json !== void 0) commandInput = {
950
+ ...commandInput,
951
+ json: raw.json
952
+ };
953
+ if (raw.envMode !== void 0) commandInput = {
954
+ ...commandInput,
955
+ envMode: raw.envMode
956
+ };
957
+ if (raw.agent !== void 0) commandInput = {
958
+ ...commandInput,
959
+ agent: raw.agent
960
+ };
961
+ if (Object.keys(filteredToolVars).length > 0) commandInput = {
962
+ ...commandInput,
963
+ toolVars: filteredToolVars
964
+ };
965
+ return ResolveModeCommand.make(commandInput);
966
+ })())), Cell.decide(resolveModeWorkflow), Cell.encode((outcome) => outcome), Cell.write((outcome) => Result.match(outcome, {
967
+ onFailure: (error) => Effect.fail(error),
968
+ onSuccess: (mode) => Effect.succeed(mode)
969
+ })));
970
+ const detectModeWithProbe = (flags = {}) => Cell.apply(outputModeProbeDescription, flags).pipe(Effect.mapError((error) => CliError.InvalidValue.make({
971
+ option: error.option,
972
+ value: error.value,
973
+ expected: error.expected,
974
+ kind: "flag"
975
+ })));
976
+ const OutputModeProbeLive = Layer.succeed(OutputModeProbe, OutputModeProbe.of({ detectMode: detectModeWithProbe({}) }));
918
977
  /**
919
978
  * Machine mode emits the U4 verdict envelope for a run that produced no
920
979
  * mutants and no report file: a `--survivors` run with zero survivors (AE3)
@@ -935,11 +994,18 @@ function emitNullScoreVerdict(stream, mode, thresholds, config, basePath, pathSe
935
994
  name: "StrykerJS",
936
995
  version: strykerVersion
937
996
  }
938
- }, mode.mode, mode.signal, stream.runId, basePath, [], pathService);
939
- stream.sink({
940
- kind: "verdict",
941
- ...envelope
942
- });
997
+ }, mode.mode, mode.signal, stream.runId, basePath, pathService);
998
+ return Queue.offer(stream.queue, VerdictReached.make({
999
+ schemaVersion: envelope.schemaVersion,
1000
+ runId: envelope.runId,
1001
+ mode: envelope.mode,
1002
+ signal: envelope.signal,
1003
+ score: envelope.score,
1004
+ thresholds: envelope.thresholds,
1005
+ reportFile: envelope.reportFile,
1006
+ counts: envelope.counts,
1007
+ mutants: envelope.mutants
1008
+ }));
943
1009
  }
944
1010
  /**
945
1011
  * Emits the machine-mode output from the run's finalizer — it runs on
@@ -954,50 +1020,70 @@ function emitNullScoreVerdict(stream, mode, thresholds, config, basePath, pathSe
954
1020
  * `--dryRunOnly` early return): then a null-score `verdict` closes the
955
1021
  * stream so the last stdout line is always a terminal event (R5).
956
1022
  */
957
- function emitMachineModeOutput(stream, mode, exit, code, argv, basePath, pathService) {
1023
+ function emitMachineModeOutput(stream, mode, outcome, basePath, pathService) {
958
1024
  return Effect.gen(function* () {
959
1025
  const captured = readCapturedConsole();
960
- const value = failureValue(exit);
961
- if (Exit.isFailure(exit) && S.is(CliError.ShowHelp)(value) && value.errors.length === 0) {
962
- const document = {
963
- kind: "help",
964
- schemaVersion: "1.0",
965
- code: 0,
966
- help: captured
967
- };
968
- stream.sink(document);
969
- return;
970
- }
971
- if (Exit.isFailure(exit)) {
972
- stream.sink({
973
- kind: "error",
974
- ...buildErrorEnvelope(exit, code, captured, argv)
975
- });
976
- return;
977
- }
978
- if (captured.length > 0) {
979
- const document = {
980
- kind: "help",
981
- schemaVersion: "1.0",
982
- code: 0,
983
- help: captured
984
- };
985
- stream.sink(document);
1026
+ if (Result.isSuccess(outcome)) {
1027
+ if (outcome.success.help) {
1028
+ yield* Queue.offer(stream.queue, HelpRendered.make({
1029
+ schemaVersion: "1.0",
1030
+ code: 0,
1031
+ help: captured
1032
+ }));
1033
+ return;
1034
+ }
1035
+ if (captured.length > 0) {
1036
+ yield* Queue.offer(stream.queue, HelpRendered.make({
1037
+ schemaVersion: "1.0",
1038
+ code: 0,
1039
+ help: captured
1040
+ }));
1041
+ return;
1042
+ }
1043
+ if (stream.isOpen()) yield* emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
986
1044
  return;
987
1045
  }
988
- if (stream.isOpen()) emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
1046
+ const envelope = shapeEnvelope(outcome.failure, captured);
1047
+ yield* Queue.offer(stream.queue, RunFailed.make({
1048
+ schemaVersion: envelope.schemaVersion,
1049
+ code: envelope.code,
1050
+ error: envelope.error,
1051
+ remediation: envelope.remediation
1052
+ }));
989
1053
  });
990
1054
  }
991
1055
  //#endregion
992
- //#region src/survivors-bookkeeping.ts
993
- /** The path a `--survivors` run reads when no `survivorsPriorReport` is configured. */
1056
+ //#region src/Survivors.ts
1057
+ /**
1058
+ * Survivors — the survivors-admission capability.
1059
+ *
1060
+ * The prior-report decoding, source hashing, mutant conversion, and admission
1061
+ * pipeline for --survivors runs. Pure admission decision lives in
1062
+ * Survivors.workflow.ts.
1063
+ */
994
1064
  const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
995
- //#endregion
996
- //#region src/survivors-hashing.ts
997
- const { entries: objectEntries$1, fromEntries: objectFromEntries } = Object;
998
1065
  /**
999
- * The content hash of one source file.
1066
+ * The mutant shape the admission carries, named once because both the decision's
1067
+ * `Admitted` payload and the command's precomputed survivor list are the same shape.
1068
+ */
1069
+ /**
1070
+ * The prior report as a document, decoded at the boundary. Module-internal: consumers
1071
+ * get the decode function, not the schema, so the report's wire shape is not a
1072
+ * surface commitment and the codec has exactly one caller.
1000
1073
  *
1074
+ * `status` is a bare string rather than the closed status set on purpose: the decide only
1075
+ * compares it to `'Survived'`, so a report written by a newer engine that added a status
1076
+ * must not be refused for carrying one.
1077
+ */
1078
+ /**
1079
+ * Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
1080
+ * `Left` is fatal by construction — it reaches the derived error channel and no write
1081
+ * runs. A malformed report therefore never reaches the decider, and nothing here casts
1082
+ * a third-party report type.
1083
+ */
1084
+ const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
1085
+ const { entries: objectEntries, fromEntries: objectFromEntries } = Object;
1086
+ /**
1001
1087
  * Thin by design: the digest is the caller's capability, and naming the call
1002
1088
  * keeps every hashing site in the admission path reading the same way.
1003
1089
  */
@@ -1011,11 +1097,8 @@ function sourceContentHash(content, hash) {
1011
1097
  * this is the recorded side, read back out of the report.
1012
1098
  */
1013
1099
  function priorSourceHashes(priorReport, hashContent) {
1014
- return objectFromEntries(objectEntries$1(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
1100
+ return objectFromEntries(objectEntries(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
1015
1101
  }
1016
- //#endregion
1017
- //#region src/survivors-mutants.ts
1018
- const { entries: objectEntries } = Object;
1019
1102
  /**
1020
1103
  * Converts a report mutant (1-based schema location) into the internal mutant
1021
1104
  * shape a run consumes (0-based positions, absolute file name) — the exact
@@ -1025,7 +1108,7 @@ const { entries: objectEntries } = Object;
1025
1108
  * incremental differ uses.
1026
1109
  */
1027
1110
  function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
1028
- return {
1111
+ return Mutant.make({
1029
1112
  id: mutant.id,
1030
1113
  fileName: resolveAbsolutePath(file),
1031
1114
  mutatorName: mutant.mutatorName,
@@ -1040,7 +1123,7 @@ function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
1040
1123
  column: mutant.location.end.column - 1
1041
1124
  }
1042
1125
  }
1043
- };
1126
+ });
1044
1127
  }
1045
1128
  /**
1046
1129
  * The survivors of the prior report: exactly the mutants whose status is
@@ -1057,11 +1140,11 @@ function extractSurvivors(priorReport, resolveAbsolutePath) {
1057
1140
  * ranges: the report's 1-based lines with the internal 0-based columns,
1058
1141
  * relative file names, deduplicated in first-seen order.
1059
1142
  */
1060
- function survivorMutateSpans(survivors, basePath, pathService) {
1143
+ function survivorMutateSpans(survivors, basePath) {
1061
1144
  const spans = [];
1062
1145
  const seen = /* @__PURE__ */ new Set();
1063
1146
  for (const survivor of survivors) {
1064
- const file = toRelativeNormalizedFileName(survivor.fileName, basePath, pathService);
1147
+ const file = toRelativeNormalizedFileName(survivor.fileName, basePath);
1065
1148
  const { start, end } = survivor.location;
1066
1149
  const span = `${file}:${start.line + 1}:${start.column}-${end.line + 1}:${end.column}`;
1067
1150
  if (!seen.has(span)) {
@@ -1071,18 +1154,7 @@ function survivorMutateSpans(survivors, basePath, pathService) {
1071
1154
  }
1072
1155
  return spans;
1073
1156
  }
1074
- //#endregion
1075
- //#region src/survivors-report.ts
1076
- /**
1077
- * Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
1078
- * `Left` is fatal by construction — it reaches the derived error channel and no write
1079
- * runs. A malformed report therefore never reaches the decider, and nothing here casts
1080
- * a third-party report type.
1081
- */
1082
- const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
1083
- //#endregion
1084
- //#region src/cli-survivors-admission.ts
1085
- const hashContent = (content) => createHash("sha256").update(content, "utf-8").digest("hex");
1157
+ const hashContent = (content) => bytesToHex(sha256(utf8ToBytes(content)));
1086
1158
  const resolveAbsolutePath = (file) => resolve(file);
1087
1159
  /**
1088
1160
  * The survivors admission, as a description whose phases chain by type and
@@ -1095,10 +1167,9 @@ const resolveAbsolutePath = (file) => resolve(file);
1095
1167
  * stashed context back and dispatches the decision to the verdict/run,
1096
1168
  * failing the run with a rejection.
1097
1169
  */
1098
- const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext, basePath) => pipe(Cell.read((cliOptions) => Effect.flatMap(Path.Path, (pathService) => resolveSurvivorsRunOptions(cliOptions, basePath).pipe(Effect.flatMap((resolvedOptions) => {
1170
+ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext, basePath, services) => pipe(Cell.read((cliOptions) => Effect.provideContext(Effect.flatMap(Path.Path, (pathService) => resolveSurvivorsRunOptions(cliOptions, basePath).pipe(Effect.flatMap((resolvedOptions) => {
1099
1171
  const priorReportPath = priorReportPathOf(resolvedOptions);
1100
- const read = readPriorReport(priorReportPath);
1101
- return Ref.set(runContext, {
1172
+ return Effect.flatMap(readPriorReport(priorReportPath), (read) => Effect.flatMap(currentSourceHashesFor(priorReportFileKeys(read.raw)), (sourceContentHashes) => Ref.set(runContext, {
1102
1173
  resolvedOptions,
1103
1174
  priorReportPath,
1104
1175
  pathService
@@ -1107,9 +1178,9 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
1107
1178
  priorReportRaw: read.raw,
1108
1179
  priorReportFound: read.found,
1109
1180
  priorReportPath,
1110
- sourceContentHashes: currentSourceHashesFor(priorReportFileKeys(read.raw))
1111
- }));
1112
- })))), Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes }) => {
1181
+ sourceContentHashes
1182
+ }))));
1183
+ }))), services)), Cell.decode(({ resolvedOptions, priorReportRaw, priorReportFound, sourceContentHashes }) => {
1113
1184
  if (!priorReportFound) return Result.succeed(AdmitSurvivorsRunCommand.make({
1114
1185
  priorReport: void 0,
1115
1186
  currentConfig: resolvedOptions,
@@ -1133,11 +1204,12 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
1133
1204
  if (context === void 0) return Effect.die("the survivors admission read must run before its write");
1134
1205
  const { resolvedOptions, priorReportPath, pathService } = context;
1135
1206
  return Result.match(outcome, {
1136
- onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () => Effect.sync(() => emitNullScoreVerdict(stream, mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService))), Match.tag("Admitted", (admitted) => {
1207
+ onSuccess: (decision) => Match.value(decision).pipe(Match.tag("NoSurvivors", () => emitNullScoreVerdict(stream, mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService)), Match.tag("Admitted", (admitted) => {
1208
+ const admittedMutants = admitted.survivors.map((s) => Mutant.make(s));
1137
1209
  return runMutationTest({
1138
1210
  ...resolvedOptions,
1139
- survivors: admitted.survivors,
1140
- mutate: survivorMutateSpans(admitted.survivors, basePath, pathService),
1211
+ survivors: admittedMutants,
1212
+ mutate: survivorMutateSpans(admittedMutants, basePath),
1141
1213
  survivorsPriorReport: priorReportPath,
1142
1214
  incremental: false
1143
1215
  }).pipe(Effect.orDie);
@@ -1160,38 +1232,35 @@ const survivorsAdmissionDescription = (runMutationTest, stream, mode, runContext
1160
1232
  */
1161
1233
  function runSurvivorsAdmission(runMutationTest, stream, mode, cliOptions, basePath) {
1162
1234
  return Effect.gen(function* () {
1235
+ const services = yield* Effect.context();
1163
1236
  const admissionContext = yield* Ref.make(void 0);
1164
- return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext, basePath), cliOptions);
1237
+ return yield* Cell.apply(survivorsAdmissionDescription(runMutationTest, stream, mode, admissionContext, basePath, services), cliOptions);
1165
1238
  });
1166
1239
  }
1167
1240
  function resolveSurvivorsRunOptions(cliOptions, basePath) {
1168
- return readConfig(cliOptions, noopLogger, forkCoreSchema, basePath);
1241
+ return readConfig(cliOptions, basePath);
1169
1242
  }
1170
1243
  function priorReportPathOf(resolved) {
1171
1244
  const configured = resolved["survivorsPriorReport"];
1172
- return typeof configured === "string" ? configured : DEFAULT_SURVIVORS_PRIOR_REPORT;
1245
+ if (typeof configured === "string") return configured;
1246
+ return DEFAULT_SURVIVORS_PRIOR_REPORT;
1173
1247
  }
1174
1248
  function readPriorReport(priorReportPath) {
1175
- let text;
1176
- try {
1177
- text = readFileSync(priorReportPath, "utf-8");
1178
- } catch {
1179
- return {
1249
+ return Effect.gen(function* () {
1250
+ return yield* (yield* FileSystem.FileSystem).readFileString(priorReportPath).pipe(Effect.map((text) => ({
1251
+ found: true,
1252
+ raw: Result.match(S.decodeResult(S.fromJsonString(S.Unknown))(text), {
1253
+ onFailure: () => text,
1254
+ onSuccess: (value) => value
1255
+ })
1256
+ })), Effect.catchTag("PlatformError", (cause) => Match.value(cause.reason).pipe(Match.tag("NotFound", () => Effect.succeed({
1180
1257
  found: false,
1181
1258
  raw: void 0
1182
- };
1183
- }
1184
- try {
1185
- return {
1186
- found: true,
1187
- raw: JSON.parse(text)
1188
- };
1189
- } catch {
1190
- return {
1191
- found: true,
1192
- raw: text
1193
- };
1194
- }
1259
+ })), Match.orElse(() => Effect.fail(ConfigFileUnreadableError.make({
1260
+ file: priorReportPath,
1261
+ cause
1262
+ }))))));
1263
+ });
1195
1264
  }
1196
1265
  function priorReportFileKeys(raw) {
1197
1266
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
@@ -1201,116 +1270,43 @@ function priorReportFileKeys(raw) {
1201
1270
  return Object.keys(files);
1202
1271
  }
1203
1272
  function readSourceFile(file) {
1204
- try {
1205
- return readFileSync(file, "utf-8");
1206
- } catch {
1207
- return "";
1208
- }
1273
+ return Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString(file).pipe(Effect.mapError((cause) => ConfigFileUnreadableError.make({
1274
+ file,
1275
+ cause
1276
+ }))));
1209
1277
  }
1210
1278
  function currentSourceHashesFor(files) {
1211
- const hashes = {};
1212
- for (const file of files) hashes[file] = sourceContentHash(readSourceFile(file), hashContent);
1213
- return hashes;
1279
+ return Effect.map(Effect.forEach(files, (file) => Effect.map(readSourceFile(file), (content) => [file, sourceContentHash(content, hashContent)]), { concurrency: 24 }), (pairs) => Object.fromEntries(pairs));
1214
1280
  }
1215
1281
  //#endregion
1216
- //#region src/cli-run.ts
1217
- const defaultRunMutationTest = (hostOptions) => (options) => Effect.scoped(runMutationTest(defaultStages, options)).pipe(Effect.provide(makeRunLayer(hostOptions)));
1282
+ //#region src/Cli.ts
1283
+ const SIGNAL_NUMBERS = Object.freeze({
1284
+ SIGINT: 2,
1285
+ SIGTERM: 15
1286
+ });
1218
1287
  /**
1219
- * The host options a run is bound to: the sink, the mode, the timing and the
1220
- * log descriptor chosen by the mode — machine mode keeps stdout exclusively
1221
- * for the NDJSON stream, so the logging backend is pointed at stderr; human
1222
- * mode keeps the stdout sink. The fix is the descriptor, never the log level.
1288
+ * Installs the listeners and returns the reader.
1289
+ *
1290
+ * The listener records and returns: interrupting the run is the runtime's job,
1291
+ * and doing it from here would race the run's own finalizer for the stream.
1292
+ * `once` per signal, because a second delivery of the same signal cannot
1293
+ * change the answer.
1223
1294
  */
1224
- function hostOptionsOf(mode, stream) {
1225
- return {
1226
- runEventSink: stream.sink,
1227
- runId: stream.runId,
1228
- resolvedMode: mode,
1229
- runStartedAt: stream.startedAt,
1230
- basePath: resolve(process.cwd()),
1231
- reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-mutation-report/stryker-plugins")],
1232
- logSink: (line) => {
1233
- if (mode.mode === "human") process.stdout.write(line);
1234
- else process.stderr.write(line);
1235
- },
1236
- allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
1237
- };
1295
+ function observeTerminatingSignal() {
1296
+ let observed = null;
1297
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
1298
+ observed = SIGNAL_NUMBERS[signal] ?? null;
1299
+ });
1300
+ return () => observed;
1238
1301
  }
1239
- /**
1240
- * The single operation of the CLI's run cell: the impure shell that
1241
- * wraps the transport's command effect with the run bootstrap. It creates the
1242
- * run's stream from the resolved mode, binds the host options a run is
1243
- * executed with, opens the stream, runs the command effect, dispatches the
1244
- * request the handlers left, and on every outcome — success, failure and
1245
- * interruption alike — emits the machine-mode terminal event (error/help/
1246
- * null verdict) and drains the stream, returning the classed exit code as its
1247
- * value. SIGINT/SIGTERM interrupt the current fiber so the finalizer runs
1248
- * before the process exits; the code is resolved exactly once (R6), in the
1249
- * finalizer, where the terminal event's `code` is chosen from the same inputs
1250
- * the teardown used before.
1251
- */
1252
- const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1253
- const stream = yield* createRunEventStream(input.mode);
1254
- const hostOptions = hostOptionsOf(input.mode, stream);
1255
- const runMutationTestImpl = input.runMutationTest ?? defaultRunMutationTest(hostOptions);
1256
- const basePath = hostOptions.basePath;
1257
- const pathService = yield* Path.Path.pipe(Effect.provide(NodePath.layer));
1258
- let currentFiber = null;
1259
- const verdictOf = (value) => {
1260
- if (!Predicate.hasProperty(value, "verdict")) return [];
1261
- const candidate = value.verdict;
1262
- if (typeof candidate !== "number" || !isExitClass(candidate)) return [];
1263
- return [candidate];
1264
- };
1265
- const resolveClassedExitCode = (exit) => {
1266
- const signal = input.lastSignal();
1267
- if (signal !== null) return 128 + signal;
1268
- if (Exit.isFailure(exit)) return resolveCliExitCode(exit);
1269
- return resolveExitCode(verdictOf(exit.value), null);
1270
- };
1271
- const onSignal = () => {
1272
- process.removeListener("SIGINT", onSignal);
1273
- process.removeListener("SIGTERM", onSignal);
1274
- if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
1275
- };
1276
- const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => runRequest.survivors ? runSurvivorsAdmission(runMutationTestImpl, stream, input.mode, runRequest.options, basePath).pipe(Effect.provide(makeRunLayer(hostOptions))) : runMutationTestImpl(runRequest.options).pipe(Effect.orDie)), Match.tag("llms", (llmsRequest) => Effect.sync(() => {
1277
- stream.ensureOpen({
1278
- mode: "machine",
1279
- signal: "flag",
1280
- stdoutIsTTY: process.stdout.isTTY === true
1281
- });
1282
- stream.sink(llmsRequest.document);
1283
- })), Match.orElse(() => Effect.die("unreachable cli request variant")));
1284
- const program = Effect.acquireUseRelease(Effect.sync(() => {
1285
- currentFiber = Fiber.getCurrent() ?? null;
1286
- process.on("SIGINT", onSignal);
1287
- process.on("SIGTERM", onSignal);
1288
- }), () => Effect.gen(function* () {
1289
- yield* stream.open;
1290
- yield* input.program;
1291
- const request = yield* Ref.get(input.requestRef);
1292
- return yield* Option.match(request, {
1293
- onNone: () => Effect.void,
1294
- onSome: (cliRequest) => dispatch(cliRequest)
1295
- });
1296
- }), () => Effect.sync(() => {
1297
- process.removeListener("SIGINT", onSignal);
1298
- process.removeListener("SIGTERM", onSignal);
1299
- }));
1300
- return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
1301
- const exit = yield* Effect.exit(restore(program));
1302
- const code = resolveClassedExitCode(exit);
1303
- if (input.mode.mode === "machine") yield* emitMachineModeOutput(stream, input.mode, exit, code, input.argv, basePath, pathService);
1304
- yield* stream.closeAndDrain;
1305
- return code;
1306
- }));
1307
- });
1308
1302
  function isObject(value) {
1309
1303
  return typeof value === "object" && value !== null && !Array.isArray(value);
1310
1304
  }
1311
1305
  function stringField(node, key) {
1312
1306
  const value = node[key];
1313
- return typeof value === "string" ? value : void 0;
1307
+ return (() => {
1308
+ if (typeof value === "string") return value;
1309
+ })();
1314
1310
  }
1315
1311
  function stringArrayField(node, key) {
1316
1312
  const value = node[key];
@@ -1359,7 +1355,10 @@ const PRIMITIVE_KIND = {
1359
1355
  };
1360
1356
  function kindOf(primitive) {
1361
1357
  const tag = stringField(primitive, "_tag");
1362
- return tag === void 0 ? "unknown" : PRIMITIVE_KIND[tag] ?? tag;
1358
+ return (() => {
1359
+ if (tag === void 0) return "unknown";
1360
+ return PRIMITIVE_KIND[tag] ?? tag;
1361
+ })();
1363
1362
  }
1364
1363
  function choiceValues(primitive) {
1365
1364
  const keys = primitive["choiceKeys"];
@@ -1368,8 +1367,13 @@ function choiceValues(primitive) {
1368
1367
  for (const key of keys) if (typeof key === "string") values.push(key);
1369
1368
  return values;
1370
1369
  }
1371
- /** The allowed reporter names, read from the U9 registry — the same list the plugin loader accepts. */
1372
- const REPORTER_NAMES = strykerPlugins.filter((plugin) => plugin.kind === PluginKind.Reporter).map((plugin) => plugin.name);
1370
+ const REPORTER_NAMES = [
1371
+ "clear-text",
1372
+ "progress",
1373
+ "html",
1374
+ "json",
1375
+ "progress-stream"
1376
+ ];
1373
1377
  /**
1374
1378
  * v4 option descriptions are stored as `Option.some(string)` on the compiled
1375
1379
  * `Single`; the walker unwraps the option.
@@ -1380,16 +1384,25 @@ function descriptionOf(single) {
1380
1384
  switch (tagOf(description)) {
1381
1385
  case "Some": {
1382
1386
  const value = description["value"];
1383
- return typeof value === "string" ? value : "";
1387
+ return (() => {
1388
+ if (typeof value === "string") return value;
1389
+ return "";
1390
+ })();
1384
1391
  }
1385
1392
  default: return "";
1386
1393
  }
1387
1394
  }
1388
1395
  function describeSingle(single, isOptional, out) {
1389
1396
  const name = stringField(single, "name") ?? "";
1390
- const primitive = isObject(single["primitiveType"]) ? single["primitiveType"] : {};
1397
+ const primitive = (() => {
1398
+ if (isObject(single["primitiveType"])) return single["primitiveType"];
1399
+ return {};
1400
+ })();
1391
1401
  const kind = kindOf(primitive);
1392
- const choices = name === "reporters" ? REPORTER_NAMES : kind === "choice" ? choiceValues(primitive) : void 0;
1402
+ const choices = (() => {
1403
+ if (name === "reporters") return REPORTER_NAMES;
1404
+ if (kind === "choice") return choiceValues(primitive);
1405
+ })();
1393
1406
  const description = descriptionOf(single);
1394
1407
  const required = kind !== "boolean" && !isOptional;
1395
1408
  const described = {
@@ -1397,7 +1410,10 @@ function describeSingle(single, isOptional, out) {
1397
1410
  aliases: stringArrayField(single, "aliases"),
1398
1411
  kind,
1399
1412
  required,
1400
- ...choices !== void 0 ? { choices } : {},
1413
+ ...(() => {
1414
+ if (choices !== void 0) return { choices };
1415
+ return {};
1416
+ })(),
1401
1417
  description
1402
1418
  };
1403
1419
  if (single["kind"] === "argument") {
@@ -1416,7 +1432,9 @@ function walkConfigNode(node, orderedParams, out) {
1416
1432
  switch (tagOf(node)) {
1417
1433
  case "Param": {
1418
1434
  const index = node["index"];
1419
- const param = typeof index === "number" ? orderedParams[index] : void 0;
1435
+ const param = (() => {
1436
+ if (typeof index === "number") return orderedParams[index];
1437
+ })();
1420
1438
  if (param !== void 0) walkParam(param, false, out);
1421
1439
  return;
1422
1440
  }
@@ -1440,7 +1458,9 @@ function describeCommandNode(node) {
1440
1458
  };
1441
1459
  const config = node["config"];
1442
1460
  if (isObject(config) && isObject(config["tree"])) {
1443
- const orderedParams = Array.isArray(config["orderedParams"]) ? config["orderedParams"] : [];
1461
+ let orderedParams = [];
1462
+ const maybeOrdered = config["orderedParams"];
1463
+ if (Array.isArray(maybeOrdered)) orderedParams = maybeOrdered;
1444
1464
  walkConfigTree(config["tree"], orderedParams, out);
1445
1465
  }
1446
1466
  const subcommands = [];
@@ -1454,17 +1474,28 @@ function describeCommandNode(node) {
1454
1474
  }
1455
1475
  return {
1456
1476
  name: stringField(node, "name") ?? "",
1457
- description: typeof node["description"] === "string" ? node["description"] : "",
1477
+ description: (() => {
1478
+ if (typeof node["description"] === "string") return node["description"];
1479
+ return "";
1480
+ })(),
1458
1481
  options: out.flags,
1459
1482
  args: out.args,
1460
1483
  subcommands
1461
1484
  };
1462
1485
  }
1463
- /**
1464
- * Builds the manifest document for a command, walking its compiled form (the
1465
- * same structure the parser matches against). `version` is the tool version,
1466
- * passed in so this module stays free of package state.
1467
- */
1486
+ function readCoreEntries() {
1487
+ try {
1488
+ const manifestPath = createRequire(import.meta.url).resolve("@systemfsoftware/stryker-js-platform-node/package.json");
1489
+ const raw = readFileSync(manifestPath, "utf-8");
1490
+ const parsed = JSON.parse(raw);
1491
+ const ExportsSchema = S.Struct({ exports: S.Record(S.String, S.Unknown) });
1492
+ const decoded = S.decodeUnknownResult(ExportsSchema)(parsed);
1493
+ if (Result.isSuccess(decoded)) return Object.keys(decoded.success.exports).filter((key) => key !== "./package.json");
1494
+ return [];
1495
+ } catch {
1496
+ return [];
1497
+ }
1498
+ }
1468
1499
  function buildLLMSManifest(command, version) {
1469
1500
  const root = describeCommandNode(command) ?? {
1470
1501
  name: "",
@@ -1477,15 +1508,14 @@ function buildLLMSManifest(command, version) {
1477
1508
  schemaVersion: "1.0",
1478
1509
  tool: root.name,
1479
1510
  version,
1480
- commands: [root]
1511
+ commands: [root],
1512
+ entries: readCoreEntries()
1481
1513
  };
1482
1514
  }
1483
1515
  /** The manifest as one JSON document, ready for stdout — the U4 convention. */
1484
1516
  function emitLLMSManifest(command, version) {
1485
1517
  return JSON.stringify(buildLLMSManifest(command, version));
1486
1518
  }
1487
- //#endregion
1488
- //#region src/stryker-cli.ts
1489
1519
  function createSplitter(separator) {
1490
1520
  return (value) => value.split(separator).filter(Boolean);
1491
1521
  }
@@ -1498,7 +1528,10 @@ const splitOnSpace = createSplitter(" ");
1498
1528
  */
1499
1529
  function parseCleanDirOption(value) {
1500
1530
  const v = value.toLocaleLowerCase();
1501
- return v === "always" ? v : v !== "false" && v !== "0";
1531
+ return (() => {
1532
+ if (v === "always") return v;
1533
+ return v !== "false" && v !== "0";
1534
+ })();
1502
1535
  }
1503
1536
  /**
1504
1537
  * Commander characterization: a pure integer is parsed as a number, anything
@@ -1516,31 +1549,12 @@ const optional = (option) => Flag.optional(option);
1516
1549
  * `Option.some(false)` for an explicit `--no-x`, so both map back to
1517
1550
  * `undefined` and leave the config-file default in force (KTD4).
1518
1551
  */
1519
- const absentWhenFalse = (value) => Option.isSome(value) && value.value ? true : void 0;
1520
- const LOG_LEVELS = [
1521
- "fatal",
1522
- "error",
1523
- "warn",
1524
- "info",
1525
- "debug",
1526
- "trace",
1527
- "off"
1528
- ];
1529
- const LOG_LEVEL_LOOKUP = {
1530
- fatal: true,
1531
- error: true,
1532
- warn: true,
1533
- info: true,
1534
- debug: true,
1535
- trace: true,
1536
- off: true
1552
+ const absentWhenFalse = (value) => {
1553
+ if (Option.isSome(value) && value.value) return true;
1537
1554
  };
1538
- function isLogLevel(value) {
1539
- return LOG_LEVEL_LOOKUP[value] === true;
1540
- }
1541
1555
  function setLogLevel(target, key, value) {
1542
1556
  const unwrapped = unwrap(value);
1543
- if (unwrapped !== void 0 && isLogLevel(unwrapped)) target[key] = unwrapped;
1557
+ if (unwrapped !== void 0) target[key] = unwrapped;
1544
1558
  }
1545
1559
  const runOptions = {
1546
1560
  ignorePatterns: Flag.string("ignorePatterns").pipe(Flag.withDescription("A comma separated list of patterns used for specifying which files need to be ignored. This should only be used in cases where you experience a slow Stryker startup, because too many (or too large) files are copied to the sandbox that are not needed to run the tests. For example, image or movie directories. Note: This option will have NO effect when using the `--inPlace` option. The directories `node_modules`, `.git` and some others are always ignored. Example: `--ignorePatterns dist`. These patterns are ALWAYS ignored: [`node_modules`, `.git`, `/reports`, `*.tsbuildinfo`, `/stryker.log`, `.stryker-tmp`]. Because Stryker always ignores these, you should rarely have to adjust the `ignorePatterns` setting at all. This is useful to speed up Stryker by reducing the size of the sandbox directory which has a positive effect on performance."), Flag.map(splitOnComma), optional),
@@ -1572,8 +1586,24 @@ const runOptions = {
1572
1586
  concurrency: Flag.string("concurrency").pipe(Flag.withAlias("c"), Flag.withDescription("Set the concurrency of workers. Stryker will always run checkers and test runners in parallel by creating worker processes (default: cpuCount - 1)"), Flag.map(parseConcurrency), optional),
1573
1587
  disableBail: Flag.map(optional(Flag.boolean("disableBail")), absentWhenFalse).pipe(Flag.withDescription("Force the test runner to keep running tests, even when a mutant is already killed.")),
1574
1588
  maxTestRunnerReuse: Flag.integer("maxTestRunnerReuse").pipe(Flag.withDescription("Restart each test runner worker process after `n` runs. Not recommended unless you are experiencing memory leaks that you are unable to resolve. Configuring `0` here means infinite reuse."), optional),
1575
- logLevel: Flag.choice("logLevel", LOG_LEVELS).pipe(Flag.withDescription(`Set the log level for the console. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${RENDERED_OPTION_DEFAULTS.logLevel}"`), optional),
1576
- fileLogLevel: Flag.choice("fileLogLevel", LOG_LEVELS).pipe(Flag.withDescription(`Set the log level for the "stryker.log" file. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${RENDERED_OPTION_DEFAULTS.fileLogLevel}"`), optional),
1589
+ logLevel: Flag.choice("logLevel", [
1590
+ "fatal",
1591
+ "error",
1592
+ "warn",
1593
+ "info",
1594
+ "debug",
1595
+ "trace",
1596
+ "off"
1597
+ ]).pipe(Flag.withDescription(`Set the log level for the console. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${RENDERED_OPTION_DEFAULTS.logLevel}"`), optional),
1598
+ fileLogLevel: Flag.choice("fileLogLevel", [
1599
+ "fatal",
1600
+ "error",
1601
+ "warn",
1602
+ "info",
1603
+ "debug",
1604
+ "trace",
1605
+ "off"
1606
+ ]).pipe(Flag.withDescription(`Set the log level for the "stryker.log" file. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${RENDERED_OPTION_DEFAULTS.fileLogLevel}"`), optional),
1577
1607
  inPlace: Flag.map(optional(Flag.boolean("inPlace")), absentWhenFalse).pipe(Flag.withDescription("Determines whether or not Stryker should mutate your files in place. Note: mutating your files in place is generally not needed for mutation testing, unless you have a dependency in your project that is really dependent on the file locations (like \"app-root-path\" for example).\nWhen `true`, Stryker will override your files, but it will keep a copy of the originals in the temp directory (using `tempDirName`) and it will place the originals back after it is done. Also with `true` the `ignorePatterns` has no effect any more.\nWhen `false` (default) Stryker will work in the copy of your code inside the temp directory.")),
1578
1608
  tempDirName: Flag.string("tempDirName").pipe(Flag.withDescription("Set the name of the directory that is used by Stryker as a working directory. This directory will be cleaned after a successful run"), optional),
1579
1609
  cleanTempDir: Flag.string("cleanTempDir").pipe(Flag.withDescription(`Choose whether or not to clean the temp dir (which is "${RENDERED_OPTION_DEFAULTS.tempDirName}" inside the current working directory by default) after a run.\n- false: Never delete the temp dir;\n- true: Delete the tmp dir after a successful run;\n- always: Always delete the temp dir, regardless of whether the run was successful.`), Flag.map(parseCleanDirOption), optional),
@@ -1658,7 +1688,7 @@ function makeStrykerCommand(requestRef) {
1658
1688
  const strykerCommand = Command.make("stryker", rootConfig, (config) => {
1659
1689
  if (config.llms === true) {
1660
1690
  const document = {
1661
- kind: "manifest",
1691
+ _tag: "manifest",
1662
1692
  schemaVersion: "1.0",
1663
1693
  code: 0,
1664
1694
  manifest: emitLLMSManifest(strykerCommand, strykerVersion)
@@ -1706,21 +1736,18 @@ const cliLayer = Layer.mergeAll(CliConfig.layer({ builtIns: [
1706
1736
  GlobalFlag.Wizard,
1707
1737
  GlobalFlag.Completions,
1708
1738
  GlobalFlag.LogLevel
1709
- ] }), Path.layer, FileSystem.layerNoop({}), terminalLayer, NodeStdio.layer, NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath$1.layer))));
1710
- /**
1711
- * The transport entry: builds the command tree, resolves the mode once at
1712
- * the edge (never a second probe), provides the CLI and Console layers the
1713
- * framework renders through, delegates the whole run to the executor cell and
1714
- * returns the classed exit code it computes. The executor is the I/O
1715
- * sandwich; this function only frames it.
1716
- */
1739
+ ] }), Path.layer, FileSystem.layerNoop({}), terminalLayer, NodeStdio.layer, NodeChildProcessSpawner.layer.pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))));
1717
1740
  function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStream, lastSignal) {
1718
1741
  return Effect.gen(function* () {
1719
- const mode = detectMode();
1742
+ const mode = yield* detectMode;
1720
1743
  const requestRef = yield* Ref.make(Option.none());
1721
1744
  const command = makeStrykerCommand(requestRef);
1722
- const cliEffect = Command.runWith(command, { version: strykerVersion })(argv).pipe(Effect.provide(Layer.mergeAll(mode.mode === "machine" ? machineConsoleLayer : Layer.empty, cliLayer)));
1723
- const outcome = yield* Effect.result(runStrykerCli({
1745
+ const consoleLayer = (() => {
1746
+ if (mode.mode === "machine") return machineConsoleLayer;
1747
+ return Layer.empty;
1748
+ })();
1749
+ const cliEffect = Command.runWith(command, { version: strykerVersion })(argv).pipe(Effect.provide(Layer.mergeAll(consoleLayer, cliLayer)));
1750
+ const result = yield* Effect.result(runStrykerCli({
1724
1751
  program: cliEffect,
1725
1752
  requestRef,
1726
1753
  mode,
@@ -1728,18 +1755,105 @@ function strykerCliEffect(argv, runMutationTest, detectMode, createRunEventStrea
1728
1755
  argv,
1729
1756
  lastSignal
1730
1757
  }, createRunEventStream));
1731
- return Result.isFailure(outcome) ? outcome.failure : outcome.success;
1732
- });
1758
+ if (Result.isFailure(result)) return result.failure;
1759
+ return result.success;
1760
+ }).pipe(Effect.orElseSucceed(() => 2));
1761
+ }
1762
+ const defaultRunMutationTest = (hostOptions, queue) => (options) => Effect.scoped(runMutationTest(options)).pipe(Effect.provideService(RunEvents, queue), Effect.provide(makeRunLayer(hostOptions)));
1763
+ function hostOptionsOf(mode, stream) {
1764
+ return {
1765
+ runId: stream.runId,
1766
+ resolvedMode: mode,
1767
+ runStartedAt: stream.startedAt,
1768
+ basePath: resolve(process.cwd()),
1769
+ reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter"), import.meta.resolve("@systemfsoftware/stryker-js-platform-node/builtin-reporters")],
1770
+ allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
1771
+ };
1733
1772
  }
1773
+ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1774
+ const stream = yield* createRunEventStream(input.mode);
1775
+ const hostOptions = hostOptionsOf(input.mode, stream);
1776
+ const runMutationTestImpl = input.runMutationTest ?? defaultRunMutationTest(hostOptions, stream.queue);
1777
+ const basePath = hostOptions.basePath;
1778
+ const pathService = yield* Path.Path.pipe(Effect.provide(NodePath.layer));
1779
+ let currentFiber = null;
1780
+ const onSignal = () => {
1781
+ process.removeListener("SIGINT", onSignal);
1782
+ process.removeListener("SIGTERM", onSignal);
1783
+ if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
1784
+ };
1785
+ const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => (() => {
1786
+ if (runRequest.survivors) return runSurvivorsAdmission(runMutationTestImpl, stream, input.mode, runRequest.options, basePath).pipe(Effect.provide(makeRunLayer(hostOptions)));
1787
+ return runMutationTestImpl(runRequest.options).pipe(Effect.orDie);
1788
+ })()), Match.tag("llms", (llmsRequest) => Effect.gen(function* () {
1789
+ stream.ensureOpen({
1790
+ mode: "machine",
1791
+ signal: "flag",
1792
+ stdoutIsTTY: process.stdout.isTTY === true
1793
+ });
1794
+ yield* Queue.offer(stream.queue, ManifestRendered.make({
1795
+ schemaVersion: "1.0",
1796
+ code: 0,
1797
+ manifest: llmsRequest.document.manifest
1798
+ }));
1799
+ })), Match.orElse(() => Effect.die("unreachable cli request variant")));
1800
+ const program = Effect.acquireUseRelease(Effect.sync(() => {
1801
+ currentFiber = Fiber.getCurrent() ?? null;
1802
+ process.on("SIGINT", onSignal);
1803
+ process.on("SIGTERM", onSignal);
1804
+ }), () => Effect.gen(function* () {
1805
+ yield* stream.open;
1806
+ yield* input.program;
1807
+ const request = yield* Ref.get(input.requestRef);
1808
+ return yield* Option.match(request, {
1809
+ onNone: () => Effect.void,
1810
+ onSome: (cliRequest) => dispatch(cliRequest)
1811
+ });
1812
+ }), () => Effect.sync(() => {
1813
+ process.removeListener("SIGINT", onSignal);
1814
+ process.removeListener("SIGTERM", onSignal);
1815
+ }));
1816
+ return yield* Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
1817
+ const outcome = classifyRunOutcome(yield* Effect.exit(restore(program)), input.lastSignal(), input.argv);
1818
+ const code = runOutcomeCode(outcome);
1819
+ if (input.mode.mode === "machine") yield* emitMachineModeOutput(stream, input.mode, outcome, basePath, pathService);
1820
+ yield* stream.closeAndDrain;
1821
+ return code;
1822
+ }));
1823
+ });
1824
+ //#endregion
1825
+ //#region src/StreamFile.ts
1826
+ const STREAM_FILE_DIR = "reports";
1827
+ const STREAM_FILE_NAME = "mutation-stream.jsonl";
1828
+ const encodeUtf8 = (line) => new TextEncoder().encode(line);
1829
+ const drainStreamFile = (framed) => Effect.gen(function* () {
1830
+ const fs = yield* FileSystem.FileSystem;
1831
+ const file = (yield* Path.Path).join(STREAM_FILE_DIR, STREAM_FILE_NAME);
1832
+ yield* fs.makeDirectory(STREAM_FILE_DIR, { recursive: true }).pipe(Effect.orDie);
1833
+ yield* Stream.run(framed.pipe(Stream.map(encodeUtf8)), fs.sink(file, { flag: "w" })).pipe(Effect.orDie);
1834
+ });
1835
+ const RunEventStreamFileLive = Layer.effect(RunEventStreamPort, Effect.gen(function* () {
1836
+ const stdio = yield* Stdio.Stdio;
1837
+ const fs = yield* FileSystem.FileSystem;
1838
+ const path = yield* Path.Path;
1839
+ const drainFramed = (framed) => drainStreamFile(framed).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
1840
+ return RunEventStreamPort.of({ createRunEventStream: (resolved) => makeRunEventStream(stdio, resolved, drainFramed) });
1841
+ }));
1734
1842
  //#endregion
1735
1843
  //#region src/main.ts
1736
1844
  const EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER = 1;
1737
1845
  process.title = "stryker";
1738
1846
  const lastSignal = observeTerminatingSignal();
1739
1847
  function isSupportedNodeVersion(version) {
1740
- const withoutV = version.startsWith("v") ? version.slice(1) : version;
1741
- const dashBase = withoutV.split("-")[0] ?? withoutV;
1742
- const parts = (dashBase.split("+")[0] ?? dashBase).split(".").map((p) => Number.parseInt(p, 10));
1848
+ let withoutV = version;
1849
+ if (version.startsWith("v")) withoutV = version.slice(1);
1850
+ const dashBaseRaw = withoutV.split("-")[0];
1851
+ let dashBase = withoutV;
1852
+ if (dashBaseRaw !== void 0) dashBase = dashBaseRaw;
1853
+ const baseRaw = dashBase.split("+")[0];
1854
+ let base = dashBase;
1855
+ if (baseRaw !== void 0) base = baseRaw;
1856
+ const parts = base.split(".").map((p) => Number.parseInt(p, 10));
1743
1857
  const major = parts[0] ?? 0;
1744
1858
  const minor = parts[1] ?? 0;
1745
1859
  const patch = parts[2] ?? 0;
@@ -1753,7 +1867,7 @@ const program = Effect.gen(function* () {
1753
1867
  const outputMode = yield* OutputModeProbe;
1754
1868
  const runEvents = yield* RunEventStreamPort;
1755
1869
  return yield* strykerCliEffect(process.argv.slice(2), void 0, outputMode.detectMode, runEvents.createRunEventStream, lastSignal);
1756
- }).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamLive).pipe(Layer.provide(NodeStdio.layer))));
1870
+ }).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamFileLive).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem.layer, NodePath.layer)))));
1757
1871
  NodeRuntime.runMain(program, {
1758
1872
  disableErrorReporting: true,
1759
1873
  teardown: (exit, onExit) => {
@@ -1762,7 +1876,8 @@ NodeRuntime.runMain(program, {
1762
1876
  return;
1763
1877
  }
1764
1878
  const signal = lastSignal();
1765
- onExit(signal === null ? EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER : 128 + signal);
1879
+ if (signal === null) onExit(EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER);
1880
+ else onExit(128 + signal);
1766
1881
  }
1767
1882
  });
1768
1883
  //#endregion