@systemfsoftware/stryker-js-cli 6.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as nodePlatformLayer } from "./node-D-ynY_kk.mjs";
2
+ import { n as nodePlatformLayer } from "./node-B7U4hEuW.mjs";
3
3
  import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem";
4
4
  import * as NodePath from "@effect/platform-node-shared/NodePath";
5
5
  import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
@@ -8,22 +8,20 @@ import * as Effect from "effect/Effect";
8
8
  import * as Exit from "effect/Exit";
9
9
  import * as Layer from "effect/Layer";
10
10
  import * as Logger from "effect/Logger";
11
+ import * as Option from "effect/Option";
11
12
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner";
12
13
  import { ConfigFileUnreadableError, buildVerdictEnvelope, defaultOptions, generateRunId, makeRunLayer, readConfig, runMutationTest, strykerVersion, toRelativeNormalizedFileName } from "@systemfsoftware/stryker-js-engine";
13
14
  import { Mutant, causeText } from "@systemfsoftware/stryker-js/Mutant";
14
- import { Heartbeat, HelpRendered, ManifestRendered, RunEvents, RunFailed, RunStarted, VerdictReached } from "@systemfsoftware/stryker-js/Run";
15
- import { RENDERED_OPTION_DEFAULTS } from "@systemfsoftware/stryker-js/Schema";
15
+ import { Heartbeat, HelpRendered, RunEvents, RunFailed, RunStarted, VerdictReached } from "@systemfsoftware/stryker-js/Run";
16
+ import { RENDERED_OPTION_DEFAULTS, StrykerOptionsSchema } from "@systemfsoftware/stryker-js/Schema";
16
17
  import * as Cause from "effect/Cause";
17
18
  import * as Console from "effect/Console";
18
19
  import * as Fiber from "effect/Fiber";
19
- import * as FileSystem from "effect/FileSystem";
20
20
  import * as Match from "effect/Match";
21
- import * as Option from "effect/Option";
22
21
  import * as Path from "effect/Path";
23
22
  import * as Queue from "effect/Queue";
24
23
  import * as Ref from "effect/Ref";
25
24
  import * as Result from "effect/Result";
26
- import * as S from "effect/Schema";
27
25
  import * as Terminal from "effect/Terminal";
28
26
  import * as Argument from "effect/unstable/cli/Argument";
29
27
  import * as CliConfig from "effect/unstable/cli/CliConfig";
@@ -32,10 +30,17 @@ import * as Command from "effect/unstable/cli/Command";
32
30
  import * as Flag from "effect/unstable/cli/Flag";
33
31
  import * as GlobalFlag from "effect/unstable/cli/GlobalFlag";
34
32
  import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
33
+ import * as Arr from "effect/Array";
34
+ import * as S from "effect/Schema";
35
35
  import { ExitClass, highestExitClass } from "@systemfsoftware/stryker-js/ExitClass";
36
36
  import * as Clock from "effect/Clock";
37
37
  import * as Formatter from "effect/Formatter";
38
38
  import * as Predicate from "effect/Predicate";
39
+ import { LocationSchema, MutantStatusSchema, MutationTestResultSchema } from "@systemfsoftware/stryker-js/Report";
40
+ import { makeHtmlReporter } from "@systemfsoftware/stryker-js-html-reporter";
41
+ import { calculateMetrics } from "@systemfsoftware/stryker-js/Metrics";
42
+ import { MutationTestReportReady } from "@systemfsoftware/stryker-js/Reporter";
43
+ import * as FileSystem from "effect/FileSystem";
39
44
  import * as Context from "effect/Context";
40
45
  import * as Stdio from "effect/Stdio";
41
46
  import * as Stream from "effect/Stream";
@@ -46,10 +51,6 @@ import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
46
51
  var engines = { "node": ">=20.0.0" };
47
52
  //#endregion
48
53
  //#region src/admit-survivors-run.workflow.ts
49
- /**
50
- * The mutant shape the admission carries, named once because both the decision's
51
- * `Admitted` payload and the command's precomputed survivor list are the same shape.
52
- */
53
54
  const MutantShape = S.Struct({
54
55
  id: S.String,
55
56
  fileName: S.String,
@@ -89,118 +90,38 @@ const PriorReportDocument = S.Struct({
89
90
  }))
90
91
  }))
91
92
  });
92
- /**
93
- * U8 — survivor re-run admission (R10, R11, KTD6, KTD7).
94
- *
95
- * The `--survivors` run re-tests exactly the mutants that survived a previous
96
- * run. Its input is the previous run's mutation report, and the run is
97
- * admitted only when a single structural hash of the resolved options, the
98
- * recorded framework version, and the per-file source content all match the
99
- * current run (KTD6).
100
- */
101
- /**
102
- * The decision's helpers reach three language built-ins the purity gate cannot
103
- * resolve as globals, so each is bound at module scope.
104
- *
105
- * These live here, beside the decision, because `make-body-purity` follows the
106
- * decision's reachable set: a helper `admissionVerdict` calls is checked as part
107
- * of the body even though it is declared outside it. That is why the properties
108
- * covering them are in this file's in-source block rather than beside a pure helper -
109
- * testing a copy the decision does not run is worse than not testing it, because
110
- * the suite goes green either way.
111
- */
112
93
  const isArray = Array.isArray;
113
- const { fromEntries: objectFromEntries$1, keys: objectKeys } = Object;
94
+ const { entries: objectEntries$1, fromEntries: objectFromEntries$1, keys: objectKeys } = Object;
114
95
  const stringify = JSON.stringify;
115
- function isRecord(value) {
116
- return typeof value === "object" && value !== null && !isArray(value);
117
- }
118
96
  const SURVIVORS_RUN_FIRST_REMEDIATION = "run a full `stryker run` first, then re-run with --survivors";
119
97
  const SURVIVORS_BOOKKEEPING_KEYS = ["survivorsPriorReport"];
120
- /**
121
- * The resolved options without the survivors-run bookkeeping keys, so both
122
- * sides of the admission comparison describe the same configuration.
123
- */
124
98
  function stripSurvivorsKeys(config) {
125
- if (!isRecord(config)) return {};
126
- const rest = { ...config };
127
- for (const key of SURVIVORS_BOOKKEEPING_KEYS) delete rest[key];
128
- return rest;
99
+ return objectFromEntries$1(objectEntries$1(config).filter(([key]) => !SURVIVORS_BOOKKEEPING_KEYS.includes(key)));
129
100
  }
130
- /**
131
- * A report written by a survivors run embeds the bookkeeping key in its
132
- * `config`. Such a report is never a valid input for another survivors run
133
- * (KTD7): without this check the second run would either re-read a shrunken set
134
- * or re-test a stale one.
135
- */
136
101
  function wasProducedBySurvivorsRun(priorReport) {
137
- const config = priorReport.config;
138
- return isRecord(config) && "survivorsPriorReport" in config;
102
+ return Option.exists(Option.liftPredicate(priorReport.config, Match.record), (config) => SURVIVORS_BOOKKEEPING_KEYS.some((bookkeeping) => bookkeeping in config));
139
103
  }
140
- /**
141
- * Serializes the comparison input with keys sorted at every level, so the result
142
- * is a function of the data and not of key insertion order.
143
- */
144
104
  function serializeSurvivorsHashInput(input) {
145
105
  return stringify(sortKeys(input));
146
106
  }
147
107
  function sortKeys(value) {
148
- if (isArray(value)) return value.map(sortKeys);
149
- if (isRecord(value)) return objectFromEntries$1(objectKeys(value).sort().map((key) => [key, sortKeys(value[key])]));
150
- return value;
108
+ return Match.value(value).pipe(Match.when(isArray, (many) => many.map((member) => sortKeys(member))), Match.when(Match.record, (named) => objectFromEntries$1(objectKeys(named).sort().map((key) => [key, sortKeys(named[key])]))), Match.orElse((leaf) => leaf));
151
109
  }
152
- /**
153
- * The prior report's facts the decision reads: its embedded configuration, which carries
154
- * both the compared options and the survivors-run provenance marker, and the engine
155
- * version it recorded. The report's files are not here — the survivors and the per-file
156
- * source hashes derived from them need capabilities the command cannot hold, so they
157
- * arrive already computed.
158
- */
159
110
  var PriorReportFacts = class extends S.Class("PriorReportFacts")({
160
111
  config: S.Record(S.String, S.Unknown),
161
112
  frameworkVersion: S.UndefinedOr(S.String)
162
113
  }) {};
163
- /**
164
- * The command of the admission workflow: a schema class, because `Workflow.make`
165
- * constrains its first argument on the class value and a declared interface produces no
166
- * value to pass. Every field is pure data — the two capabilities the previous shape
167
- * carried, a digest function and a path resolver, can never be schema fields, so their
168
- * results arrive precomputed from the decode phase instead.
169
- */
170
114
  var AdmitSurvivorsRunCommand = class extends S.Class("AdmitSurvivorsRunCommand")({
171
- /**
172
- * The prior run's report facts, `undefined` when no report exists — the run cannot be
173
- * admitted without one ('no-report'). Explicitly nullable rather than key-optional: a
174
- * missing report is a state the edge determined and states, not a key it forgot.
175
- */
176
115
  priorReport: S.UndefinedOr(PriorReportFacts),
177
- /** The current run's resolved options (defaults + config file + CLI). */
178
116
  currentConfig: S.Record(S.String, S.Unknown),
179
- /** The current CLI/framework version (`strykerVersion`). */
180
117
  frameworkVersion: S.String,
181
- /**
182
- * Per-file content hashes of the current source, keyed by the prior report's relative
183
- * file keys. The prior side is hashed from the sources the report embeds, so an editor
184
- * save that shifts line ranges — which would silently re-test a different mutant than
185
- * the one that survived — is caught here.
186
- */
187
118
  sourceContentHashes: S.Record(S.String, S.String),
188
- /** The same hashes for the sources the prior report embeds, computed at the edge. */
189
119
  priorSourceHashes: S.Record(S.String, S.String),
190
- /** The prior report's survivors, already converted to the internal mutant shape. */
191
120
  priorSurvivors: S.Array(MutantShape)
192
121
  }) {};
193
122
  const NO_REPORT_DETAIL = "No prior mutation report found — a --survivors run needs the report of a previous run.";
194
123
  const SURVIVORS_RUN_SOURCE_DETAIL = "The prior mutation report was itself produced by a --survivors run, so it is not a valid input for another one.";
195
124
  const MISMATCH_DETAIL = "The prior mutation report does not match the current run (resolved options, framework version, or source content differ).";
196
- /**
197
- * Whether the admission inputs agree: the prior report's embedded resolved options,
198
- * framework version and source content against the current run's.
199
- *
200
- * The comparison is on the canonical serializations rather than digests of them. Equal
201
- * serializations are equal runs, so the digest was a lossy restatement of the check that
202
- * also demanded a capability no command can carry.
203
- */
204
125
  function hashesMatch(priorReport, input) {
205
126
  return serializeSurvivorsHashInput({
206
127
  resolvedOptions: stripSurvivorsKeys(priorReport.config),
@@ -232,18 +153,46 @@ function reject(reason, detail) {
232
153
  remediation: `${detail} ${SURVIVORS_RUN_FIRST_REMEDIATION}`
233
154
  }));
234
155
  }
156
+ /**
157
+ * The closed set of admissions a command can name, before any of them is turned
158
+ * into a decision. Deriving the variant first keeps the decision itself a total
159
+ * dispatch over a type rather than a fallback over predicates.
160
+ */
161
+ const PriorReportAbsentOutcome = S.TaggedStruct("PriorReportAbsent", {});
162
+ const PriorReportIsSurvivorsRunOutcome = S.TaggedStruct("PriorReportIsSurvivorsRun", {});
163
+ const NoSurvivorsFoundOutcome = S.TaggedStruct("NoSurvivorsFound", {});
164
+ const PriorReportDriftedOutcome = S.TaggedStruct("PriorReportDrifted", {});
165
+ const SurvivorsMatchOutcome = S.TaggedStruct("SurvivorsMatch", { survivors: S.Array(MutantShape) });
166
+ S.Union([
167
+ PriorReportAbsentOutcome,
168
+ PriorReportIsSurvivorsRunOutcome,
169
+ NoSurvivorsFoundOutcome,
170
+ PriorReportDriftedOutcome,
171
+ SurvivorsMatchOutcome
172
+ ]);
173
+ const ADMISSION_RULES = [
174
+ {
175
+ holds: (input) => input.priorReport === void 0,
176
+ outcome: PriorReportAbsentOutcome.make({})
177
+ },
178
+ {
179
+ holds: (input) => Option.exists(Option.fromUndefinedOr(input.priorReport), wasProducedBySurvivorsRun),
180
+ outcome: PriorReportIsSurvivorsRunOutcome.make({})
181
+ },
182
+ {
183
+ holds: (input) => input.priorSurvivors.length === 0,
184
+ outcome: NoSurvivorsFoundOutcome.make({})
185
+ },
186
+ {
187
+ holds: (input) => Option.exists(Option.fromUndefinedOr(input.priorReport), (facts) => !hashesMatch(facts, input)),
188
+ outcome: PriorReportDriftedOutcome.make({})
189
+ }
190
+ ];
191
+ const admissionOutcomeOf = (input) => Option.getOrElse(Option.map(Arr.findFirst(ADMISSION_RULES, (rule) => rule.holds(input)), (rule) => rule.outcome), () => SurvivorsMatchOutcome.make({ survivors: input.priorSurvivors }));
235
192
  function decideAdmission(input) {
236
- const priorReport = input.priorReport;
237
- if (priorReport === void 0) return reject("no-report", NO_REPORT_DETAIL);
238
- if (wasProducedBySurvivorsRun(priorReport)) return reject("mismatch", SURVIVORS_RUN_SOURCE_DETAIL);
239
- if (input.priorSurvivors.length === 0) return Result.succeed(NoSurvivors.make());
240
- if (!hashesMatch(priorReport, input)) return reject("mismatch", MISMATCH_DETAIL);
241
- return Result.succeed(Admitted.make({ survivors: input.priorSurvivors }));
242
- }
243
- function admissionDecision(command) {
244
- return decideAdmission(command);
193
+ return Match.value(admissionOutcomeOf(input)).pipe(Match.tag("PriorReportAbsent", () => reject("no-report", NO_REPORT_DETAIL)), Match.tag("PriorReportIsSurvivorsRun", () => reject("mismatch", SURVIVORS_RUN_SOURCE_DETAIL)), Match.tag("NoSurvivorsFound", () => Result.succeed(NoSurvivors.make())), Match.tag("PriorReportDrifted", () => reject("mismatch", MISMATCH_DETAIL)), Match.tag("SurvivorsMatch", (matched) => Result.succeed(Admitted.make({ survivors: matched.survivors }))), Match.exhaustive);
245
194
  }
246
- const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, admissionDecision);
195
+ const admitSurvivorsRun = Workflow.make(AdmitSurvivorsRunCommand, decideAdmission);
247
196
  //#endregion
248
197
  //#region src/classify-run-outcome.workflow.ts
249
198
  var RunOutcomeCommand = class extends S.TaggedClass()("RunOutcomeCommand", {
@@ -288,42 +237,42 @@ var RunFailed$1 = class extends S.TaggedClass()("RunFailed", {
288
237
  }) {
289
238
  [RunOutcomeTypeId] = RunOutcomeTypeId;
290
239
  };
291
- function classify(command) {
292
- if (command.signal !== void 0) return RunInterrupted.make({ code: 128 + command.signal });
293
- if (command.succeeded) {
294
- if (command.successExitClass !== void 0) return RunFailed$1.make({
295
- code: classCode(command.successExitClass),
296
- diagnostic: command.diagnostic
297
- });
298
- return RunOk.make({ help: false });
299
- }
300
- if (command.interrupted) return RunInterrupted.make({ code: 1 });
301
- if (command.helpErrorCount !== void 0) {
302
- if (command.helpErrorCount > 0) return RunParseFailed.make({ unrecognized: command.unrecognized });
303
- return RunOk.make({ help: true });
304
- }
305
- if (command.cliError) return RunParseFailed.make({ unrecognized: command.unrecognized });
306
- if (command.survivorsReason !== void 0) return RunSurvivorsRejected.make({
307
- reason: command.survivorsReason,
308
- diagnostic: command.survivorsDiagnostic
309
- });
310
- if (command.schemaError) return RunConfigFailed.make({ detail: command.configDetail });
311
- if (command.highestExitClass !== void 0) {
312
- if (command.highestExitClass === "ConfigError") return RunConfigFailed.make({ detail: command.configDetail });
313
- return RunFailed$1.make({
314
- code: classCode(command.highestExitClass),
315
- diagnostic: command.diagnostic
316
- });
317
- }
318
- return RunFailed$1.make({
319
- code: 1,
240
+ const isSignaled = (command) => command.signal !== void 0;
241
+ const isSucceeded = (command) => command.succeeded;
242
+ const isHelpRun = (command) => command.helpErrorCount !== void 0;
243
+ const isSurvivorsRun = (command) => command.survivorsReason !== void 0;
244
+ const isHighestClassed = (command) => command.highestExitClass !== void 0;
245
+ const signaledOutcome = (command) => RunInterrupted.make({ code: 128 + command.signal });
246
+ const succeededOutcome = (command) => Option.match(Option.fromUndefinedOr(command.successExitClass), {
247
+ onNone: () => RunOk.make({ help: false }),
248
+ onSome: (exitClass) => RunFailed$1.make({
249
+ code: classCode(exitClass),
320
250
  diagnostic: command.diagnostic
321
- });
251
+ })
252
+ });
253
+ const helpOutcome = (command) => Match.value(command.helpErrorCount > 0).pipe(Match.when(true, () => RunParseFailed.make({ unrecognized: command.unrecognized })), Match.orElse(() => RunOk.make({ help: true })));
254
+ const parseFailedOutcome = (command) => RunParseFailed.make({ unrecognized: command.unrecognized });
255
+ const survivorsRejectedOutcome = (command) => RunSurvivorsRejected.make({
256
+ reason: command.survivorsReason,
257
+ diagnostic: command.survivorsDiagnostic
258
+ });
259
+ const configFailedOutcome = (command) => RunConfigFailed.make({ detail: command.configDetail });
260
+ const classedOutcome = (command) => Match.value(command.highestExitClass).pipe(Match.when("ConfigError", () => RunConfigFailed.make({ detail: command.configDetail })), Match.orElse((exitClass) => RunFailed$1.make({
261
+ code: classCode(exitClass),
262
+ diagnostic: command.diagnostic
263
+ })));
264
+ const genericFailureOutcome = (command) => RunFailed$1.make({
265
+ code: 1,
266
+ diagnostic: command.diagnostic
267
+ });
268
+ function classify(command) {
269
+ return Match.value(command).pipe(Match.when(isSignaled, signaledOutcome), Match.when(isSucceeded, succeededOutcome), Match.when((interrupted) => interrupted.interrupted, () => RunInterrupted.make({ code: 1 })), Match.when(isHelpRun, helpOutcome), Match.when((cliError) => cliError.cliError, parseFailedOutcome), Match.when(isSurvivorsRun, survivorsRejectedOutcome), Match.when((schemaError) => schemaError.schemaError, configFailedOutcome), Match.when(isHighestClassed, classedOutcome), Match.orElse(genericFailureOutcome));
322
270
  }
323
271
  const classifyRunOutcome$1 = Workflow.make(RunOutcomeCommand, (command) => Match.value(classify(command)).pipe(Match.tag("RunInterrupted", (error) => Result.fail(error)), Match.when((outcome) => !(outcome instanceof RunInterrupted), (decision) => Result.succeed(decision)), Match.exhaustive));
324
272
  //#endregion
325
273
  //#region src/Envelope.ts
326
274
  const CONFIG_CODE = 2;
275
+ const UNKNOWN_FAILURE = "Unknown failure";
327
276
  function runOutcomeCode(result) {
328
277
  const outcome = Result.match(result, {
329
278
  onSuccess: (success) => success,
@@ -334,170 +283,138 @@ function runOutcomeCode(result) {
334
283
  function isExitClass(value) {
335
284
  return S.is(ExitClass)(value);
336
285
  }
286
+ const asExitClass = Option.liftPredicate(isExitClass);
337
287
  function exitClassOf(value) {
338
- if (typeof value !== "object" || value === null) return;
339
- if (!("exitClass" in value)) return;
340
- const raw = Reflect.get(value, "exitClass");
341
- if (!isExitClass(raw)) return;
342
- return raw;
343
- }
344
- function collectExitClassesFromValue(value, out, seen, depth) {
345
- if (depth > 10 || value === null || value === void 0) return;
346
- if (typeof value !== "object") return;
347
- if (seen.has(value)) return;
348
- seen.add(value);
349
- const ec = exitClassOf(value);
350
- if (ec !== void 0) out.push(ec);
351
- if ("cause" in value) {
352
- const causeVal = Reflect.get(value, "cause");
353
- if (Array.isArray(causeVal)) for (const entry of causeVal) collectExitClassesFromValue(entry, out, seen, depth + 1);
354
- else collectExitClassesFromValue(causeVal, out, seen, depth + 1);
288
+ if (!Predicate.isObjectOrArray(value)) return;
289
+ return Option.getOrUndefined(asExitClass(Reflect.get(value, "exitClass")));
290
+ }
291
+ const MAX_TRAVERSAL_DEPTH = 10;
292
+ function isUnseenObject(value, seen) {
293
+ return Predicate.isObjectOrArray(value) && !seen.has(value);
294
+ }
295
+ function isReachableValue(value, depth, seen) {
296
+ return depth <= MAX_TRAVERSAL_DEPTH && isUnseenObject(value, seen);
297
+ }
298
+ function isChildList(value) {
299
+ return Array.isArray(value);
300
+ }
301
+ function causeChildrenOf(value) {
302
+ const cause = Reflect.get(value, "cause");
303
+ return Match.value(cause).pipe(Match.when(isChildList, (children) => children), Match.orElse(() => [cause]));
304
+ }
305
+ function visitReachableValue(value, depth, seen, visit) {
306
+ if (isReachableValue(value, depth, seen)) {
307
+ seen.add(value);
308
+ visit(value);
309
+ causeChildrenOf(value).forEach((child) => visitReachableValue(child, depth + 1, seen, visit));
355
310
  }
356
311
  }
312
+ function findReachableValue(value, depth, seen, read) {
313
+ if (!isReachableValue(value, depth, seen)) return Option.none();
314
+ seen.add(value);
315
+ return Option.orElse(read(value), () => Arr.findFirst(causeChildrenOf(value), (child) => findReachableValue(child, depth + 1, seen, read)));
316
+ }
317
+ function causePayloadOf(reason) {
318
+ return Match.value(reason).pipe(Match.when(Cause.isFailReason, (failed) => failed.error), Match.when(Cause.isDieReason, (died) => died.defect), Match.orElse(() => void 0));
319
+ }
320
+ const failedExit = Option.liftPredicate(Exit.isFailure);
321
+ function failurePayloads(exit) {
322
+ return Option.getOrElse(Option.map(failedExit(exit), (failure) => failure.cause.reasons.map(causePayloadOf)), () => []);
323
+ }
324
+ function appendExitClass(value, out) {
325
+ const declared = exitClassOf(value);
326
+ if (declared !== void 0) out.push(declared);
327
+ }
357
328
  function collectExitClasses(exit) {
358
329
  const out = [];
359
330
  const seen = /* @__PURE__ */ new WeakSet();
360
- if (Exit.isFailure(exit)) for (const reason of exit.cause.reasons) {
361
- let candidate;
362
- if (Cause.isFailReason(reason)) candidate = reason.error;
363
- else if (Cause.isDieReason(reason)) candidate = reason.defect;
364
- else candidate = void 0;
365
- if (candidate !== void 0) collectExitClassesFromValue(candidate, out, seen, 0);
366
- }
331
+ failurePayloads(exit).forEach((payload) => visitReachableValue(payload, 0, seen, (value) => appendExitClass(value, out)));
367
332
  return out;
368
333
  }
334
+ function isNonEmptyString$1(value) {
335
+ return typeof value === "string" && value.length > 0;
336
+ }
337
+ const nonEmptyText = Option.liftPredicate(isNonEmptyString$1);
369
338
  function reasonOf(value) {
370
- if (!("reason" in value)) return;
371
- const reason = Reflect.get(value, "reason");
372
- if (typeof reason !== "string" || reason.length === 0) return;
373
- const detail = causeTextOf(value);
374
- if (detail === void 0) return reason;
375
- return `${reason}: ${detail}`;
339
+ const declared = Reflect.get(value, "reason");
340
+ return Option.getOrUndefined(Option.map(nonEmptyText(declared), (reason) => Option.match(Option.fromNullishOr(causeTextOf(value)), {
341
+ onNone: () => reason,
342
+ onSome: (detail) => `${reason}: ${detail}`
343
+ })));
376
344
  }
377
- function causeTextOf(value, depth = 0) {
378
- if (depth > 4 || !("cause" in value)) return;
345
+ function causeTextOf(value) {
379
346
  const cause = Reflect.get(value, "cause");
380
- return causeText(cause, depth + 1);
347
+ return causeText(cause, 1);
381
348
  }
382
- function configDetailOf(value) {
383
- if (!("reason" in value) && !("message" in value)) return;
349
+ function firstConfiguredText(value) {
384
350
  const reason = Reflect.get(value, "reason");
385
- if (typeof reason === "string" && reason.length > 0) return reason;
386
351
  const message = Reflect.get(value, "message");
387
- if (typeof message === "string" && message.length > 0) return message;
388
- }
389
- function shouldVisitConfigValue(value, depth, seen) {
390
- if (depth > 10) return false;
391
- if (value === null || value === void 0) return false;
392
- if (typeof value !== "object") return false;
393
- if (seen.has(value)) return false;
394
- return true;
395
- }
396
- function pushConfigCauses(value, depth, stack) {
397
- if (!("cause" in value)) return;
398
- const causeVal = Reflect.get(value, "cause");
399
- if (Array.isArray(causeVal)) for (let index = causeVal.length - 1; index >= 0; index--) stack.push({
400
- value: causeVal[index],
401
- depth: depth + 1
402
- });
403
- else stack.push({
404
- value: causeVal,
405
- depth: depth + 1
406
- });
352
+ return Option.orElse(nonEmptyText(reason), () => nonEmptyText(message));
353
+ }
354
+ function configDetailAt(value) {
355
+ if (exitClassOf(value) !== "ConfigError") return Option.none();
356
+ return firstConfiguredText(value);
407
357
  }
408
358
  function firstConfigErrorDetail(exit) {
409
- if (!Exit.isFailure(exit)) return;
410
359
  const seen = /* @__PURE__ */ new WeakSet();
411
- const stack = [];
412
- for (const reason of exit.cause.reasons) {
413
- let candidate;
414
- if (Cause.isFailReason(reason)) candidate = reason.error;
415
- else if (Cause.isDieReason(reason)) candidate = reason.defect;
416
- else candidate = void 0;
417
- if (candidate !== void 0) stack.push({
418
- value: candidate,
419
- depth: 0
420
- });
421
- }
422
- while (stack.length > 0) {
423
- const entry = stack.pop();
424
- if (entry === void 0) continue;
425
- const { value, depth } = entry;
426
- if (!shouldVisitConfigValue(value, depth, seen)) continue;
427
- seen.add(value);
428
- if (exitClassOf(value) === "ConfigError") {
429
- const detail = configDetailOf(value);
430
- if (detail !== void 0) return detail;
431
- }
432
- pushConfigCauses(value, depth, stack);
433
- }
360
+ const roots = [...failurePayloads(exit)].reverse();
361
+ return Option.getOrUndefined(Arr.findFirst(roots, (root) => findReachableValue(root, 0, seen, configDetailAt)));
362
+ }
363
+ const PRIMITIVE_REFINEMENTS = [
364
+ Predicate.isString,
365
+ Predicate.isNumber,
366
+ Predicate.isBoolean,
367
+ Predicate.isBigInt,
368
+ Predicate.isSymbol
369
+ ];
370
+ const isPrimitiveText = Predicate.some(PRIMITIVE_REFINEMENTS);
371
+ function isMessageError(value) {
372
+ return value instanceof Error && value.message.length > 0;
373
+ }
374
+ function declaresReasonText(value) {
375
+ return Predicate.isObjectOrArray(value) && Option.isSome(nonEmptyText(Reflect.get(value, "reason")));
376
+ }
377
+ function failureValueDescription(value) {
378
+ return Match.value(value).pipe(Match.when(S.is(SurvivorsRejection), (rejected) => Option.some(rejected.remediation)), Match.when(declaresReasonText, (carrier) => Option.fromNullishOr(reasonOf(carrier))), Match.when(isMessageError, (error) => Option.some(error.message)), Match.when(isPrimitiveText, (primitive) => Option.some(String(primitive))), Match.orElse(() => Option.none()));
379
+ }
380
+ function failureDescriptionOf(exit) {
381
+ return failureValueDescription(failureValue(exit)).pipe(Option.orElse(() => Option.fromNullishOr(firstConfigErrorDetail(exit))), Option.orElse(() => Option.flatMap(failedExit(exit), (failure) => nonEmptyText(Cause.pretty(failure.cause)))));
434
382
  }
435
383
  function describeFailure(exit) {
436
- if (!Exit.isFailure(exit)) return "Unknown failure";
437
- const value = failureValue(exit);
438
- if (value !== void 0) {
439
- if (S.is(SurvivorsRejection)(value)) return value.remediation;
440
- if (typeof value === "object" && value !== null) {
441
- const reason = reasonOf(value);
442
- if (reason !== void 0) return reason;
443
- }
444
- if (value instanceof Error && value.message.length > 0) return value.message;
445
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
446
- }
447
- if (collectExitClasses(exit).includes("ConfigError")) {
448
- const detail = firstConfigErrorDetail(exit);
449
- if (detail !== void 0) return detail;
450
- }
451
- const rendered = Cause.pretty(exit.cause);
452
- if (rendered.length > 0) return rendered;
453
- return "Unknown failure";
384
+ return Option.getOrElse(failureDescriptionOf(exit), () => UNKNOWN_FAILURE);
454
385
  }
455
386
  function unrecognizedArgumentOf(exit, argv) {
456
- if (!Exit.isFailure(exit)) return;
457
- const value = failureValue(exit);
458
- if (value === void 0 || !CliError.isCliError(value)) return;
459
- const errors = (() => {
460
- if (S.is(CliError.ShowHelp)(value)) return value.errors;
461
- return [value];
462
- })();
463
- for (const error of errors) {
464
- if (S.is(CliError.UnrecognizedOption)(error)) {
465
- const at = argv.indexOf(error.option);
466
- const next = (() => {
467
- if (at >= 0) return argv[at + 1];
468
- })();
469
- if (next !== void 0 && !next.startsWith("-")) return next;
470
- return error.option;
471
- }
472
- if (S.is(CliError.UnexpectedArgument)(error)) return error.arguments[0];
473
- if (S.is(CliError.UnknownSubcommand)(error)) return error.subcommand;
474
- }
387
+ return Option.getOrUndefined(cliErrorList(exit).pipe(Option.flatMap((errors) => Arr.findFirst(errors, (error) => argumentHintOf(error, argv)))));
388
+ }
389
+ function cliErrorList(exit) {
390
+ return Match.value(failureValue(exit)).pipe(Match.when(S.is(CliError.ShowHelp), (help) => Option.some(help.errors)), Match.when(CliError.isCliError, (cliError) => Option.some([cliError])), Match.orElse(() => Option.none()));
391
+ }
392
+ function argumentHintOf(error, argv) {
393
+ return Match.value(error).pipe(Match.tag("UnrecognizedOption", (unrecognized) => Option.some(unrecognizedArgument(argv, unrecognized.option))), Match.tag("UnexpectedArgument", (unexpected) => Option.fromNullishOr(unexpected.arguments[0])), Match.tag("UnknownSubcommand", (unknown) => Option.some(unknown.subcommand)), Match.orElse(() => Option.none()));
394
+ }
395
+ function unrecognizedArgument(argv, option) {
396
+ return followingArgument(argv, option).pipe(Option.filter((argument) => !argument.startsWith("-")), Option.getOrElse(() => option));
397
+ }
398
+ function followingArgument(argv, option) {
399
+ return Match.value(argv.indexOf(option)).pipe(Match.when((at) => at >= 0, (at) => Option.fromNullishOr(argv[at + 1])), Match.orElse(() => Option.none()));
475
400
  }
476
401
  function failureValue(exit) {
477
- if (!Exit.isFailure(exit)) return;
478
- const failure = Cause.findErrorOption(exit.cause);
479
- if (Option.isSome(failure)) return failure.value;
402
+ return Option.getOrUndefined(Option.flatMap(failedExit(exit), (failure) => Cause.findErrorOption(failure.cause)));
480
403
  }
481
404
  const SIGNAL_REMEDIATION = "the run was interrupted by a signal; re-run it to continue";
482
405
  const PARSE_REMEDIATION = "re-run with --help to see the full usage";
483
406
  const DEFAULT_REMEDIATION = "see --reportFile or the verdict envelope on stdout";
407
+ function verdictExitClass(value) {
408
+ return Match.value(value).pipe(Match.when(Predicate.hasProperty("verdict"), (carrier) => Option.getOrUndefined(asExitClass(carrier.verdict))), Match.orElse(() => void 0));
409
+ }
484
410
  function successExitClassOf(exit) {
485
- if (!Exit.isSuccess(exit)) return;
486
- const value = exit.value;
487
- if (!Predicate.hasProperty(value, "verdict")) return;
488
- const candidate = value.verdict;
489
- if (!isExitClass(candidate)) return;
490
- return candidate;
411
+ return Match.value(exit).pipe(Match.when(Exit.isSuccess, (success) => verdictExitClass(success.value)), Match.orElse(() => void 0));
491
412
  }
492
413
  function helpErrorCountOf(value) {
493
- if (value === void 0) return;
494
- if (!S.is(CliError.ShowHelp)(value)) return;
495
- return value.errors.length;
414
+ return Match.value(value).pipe(Match.when(S.is(CliError.ShowHelp), (help) => help.errors.length), Match.orElse(() => void 0));
496
415
  }
497
416
  function survivorsRejectionOf(value) {
498
- if (value === void 0) return;
499
- if (!S.is(SurvivorsRejection)(value)) return;
500
- return value;
417
+ return Match.value(value).pipe(Match.when(S.is(SurvivorsRejection), (survivors) => survivors), Match.orElse(() => void 0));
501
418
  }
502
419
  function present(value) {
503
420
  if (value === null) return;
@@ -512,12 +429,20 @@ function survivorsDiagnosticOf(survivors) {
512
429
  return survivors.remediation;
513
430
  }
514
431
  function omitUnknownFailure(diagnostic) {
515
- if (diagnostic === "Unknown failure") return;
432
+ if (diagnostic === UNKNOWN_FAILURE) return;
516
433
  return diagnostic;
517
434
  }
518
435
  function capturedOrUnknown(captured) {
519
- if (captured.length > 0) return captured;
520
- return "Unknown failure";
436
+ return Option.getOrElse(nonEmptyText(captured), () => UNKNOWN_FAILURE);
437
+ }
438
+ function hasOnlyInterrupts(exit) {
439
+ return Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
440
+ }
441
+ function carriesCliError(value) {
442
+ return value !== void 0 && CliError.isCliError(value);
443
+ }
444
+ function carriesSchemaError(value) {
445
+ return value !== void 0 && S.isSchemaError(value);
521
446
  }
522
447
  function gatherRunOutcome(exit, signal, argv) {
523
448
  const value = failureValue(exit);
@@ -525,35 +450,24 @@ function gatherRunOutcome(exit, signal, argv) {
525
450
  return RunOutcomeCommand.make({
526
451
  succeeded: Exit.isSuccess(exit),
527
452
  signal: present(signal),
528
- interrupted: Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause),
453
+ interrupted: hasOnlyInterrupts(exit),
529
454
  helpErrorCount: helpErrorCountOf(value),
530
- cliError: value !== void 0 && CliError.isCliError(value),
455
+ cliError: carriesCliError(value),
531
456
  unrecognized: unrecognizedArgumentOf(exit, argv),
532
457
  survivorsReason: survivorsReasonOf(survivors),
533
458
  survivorsDiagnostic: survivorsDiagnosticOf(survivors),
534
- schemaError: value !== void 0 && S.isSchemaError(value),
459
+ schemaError: carriesSchemaError(value),
535
460
  successExitClass: successExitClassOf(exit),
536
461
  highestExitClass: present(highestExitClass(collectExitClasses(exit))),
537
462
  configDetail: firstConfigErrorDetail(exit),
538
463
  diagnostic: omitUnknownFailure(describeFailure(exit))
539
464
  });
540
465
  }
466
+ function capturedThenRecorded(captured, recorded) {
467
+ return Option.getOrElse(nonEmptyText(captured), () => Option.getOrElse(Option.fromNullishOr(recorded), () => UNKNOWN_FAILURE));
468
+ }
541
469
  function errorText(error, captured) {
542
- return Match.value(error).pipe(Match.tag("RunParseFailed", (failed) => {
543
- if (failed.unrecognized !== void 0) return `Received unknown argument: '${failed.unrecognized}'`;
544
- return capturedOrUnknown(captured);
545
- }), Match.tag("RunSurvivorsRejected", (failed) => {
546
- if (failed.diagnostic !== void 0) return failed.diagnostic;
547
- return "Unknown failure";
548
- }), Match.tag("RunInterrupted", () => capturedOrUnknown(captured)), Match.tag("RunConfigFailed", (failed) => {
549
- if (captured.length > 0) return captured;
550
- if (failed.detail !== void 0) return failed.detail;
551
- return "Unknown failure";
552
- }), Match.tag("RunFailed", (failed) => {
553
- if (captured.length > 0) return captured;
554
- if (failed.diagnostic !== void 0) return failed.diagnostic;
555
- return "Unknown failure";
556
- }), Match.exhaustive);
470
+ return Match.value(error).pipe(Match.tag("RunParseFailed", (failed) => Option.getOrElse(Option.map(Option.fromNullishOr(failed.unrecognized), (value) => `Received unknown argument: '${value}'`), () => capturedOrUnknown(captured))), Match.tag("RunSurvivorsRejected", (failed) => Option.getOrElse(Option.fromNullishOr(failed.diagnostic), () => UNKNOWN_FAILURE)), Match.tag("RunInterrupted", () => capturedOrUnknown(captured)), Match.tag("RunConfigFailed", (failed) => capturedThenRecorded(captured, failed.detail)), Match.tag("RunFailed", (failed) => capturedThenRecorded(captured, failed.diagnostic)), Match.exhaustive);
557
471
  }
558
472
  function remediationText(error) {
559
473
  return Match.value(error).pipe(Match.tag("RunInterrupted", (failed) => {
@@ -585,35 +499,52 @@ function inspectValue(value) {
585
499
  if (typeof value === "string") return value;
586
500
  return Formatter.format(value);
587
501
  }
588
- function formatArgs(args) {
589
- if (args.length === 0) return "";
590
- const first = args[0];
591
- if (typeof first === "string") {
592
- let index = 1;
593
- let result = first.replace(/%[sdijfopO%]/g, (match) => {
594
- if (match === "%%") return "%";
595
- if (index >= args.length) return match;
596
- const arg = args[index++];
597
- switch (match) {
598
- case "%s": return String(arg);
599
- case "%d":
600
- case "%i":
601
- case "%f": return Number(arg).toString();
602
- case "%j": try {
603
- return String(JSON.stringify(arg));
604
- } catch {
605
- return "[Circular]";
606
- }
607
- case "%o":
608
- case "%O":
609
- case "%p": return inspectValue(arg);
610
- default: return match;
611
- }
612
- });
613
- for (; index < args.length; index++) result += ` ${inspectValue(args[index])}`;
614
- return result;
502
+ const PLACEHOLDER = /(%[sdijfopO%])/g;
503
+ const PLACEHOLDER_PART = /^%[sdijfopO%]$/;
504
+ function jsonArgumentText(argument) {
505
+ try {
506
+ return String(JSON.stringify(argument));
507
+ } catch {
508
+ return "[Circular]";
615
509
  }
616
- return args.map(inspectValue).join(" ");
510
+ }
511
+ function placeholderText(placeholder, argument) {
512
+ return Match.value(placeholder).pipe(Match.when(Match.is("%s"), () => String(argument)), Match.when(Match.is("%d", "%i", "%f"), () => Number(argument).toString()), Match.when(Match.is("%j"), () => jsonArgumentText(argument)), Match.when(Match.is("%o", "%O", "%p"), () => inspectValue(argument)), Match.orElse((unmatched) => unmatched));
513
+ }
514
+ function appendLiteral(progress, literal) {
515
+ return {
516
+ args: progress.args,
517
+ cursor: progress.cursor,
518
+ text: progress.text + literal
519
+ };
520
+ }
521
+ function substitutePlaceholder(progress, placeholder) {
522
+ return Match.value(progress.cursor).pipe(Match.when((cursor) => cursor < progress.args.length, (cursor) => ({
523
+ args: progress.args,
524
+ cursor: cursor + 1,
525
+ text: progress.text + placeholderText(placeholder, progress.args[cursor])
526
+ })), Match.orElse(() => appendLiteral(progress, placeholder)));
527
+ }
528
+ function consumePlaceholder(progress, placeholder) {
529
+ return Match.value(placeholder).pipe(Match.when(Match.is("%%"), () => appendLiteral(progress, "%")), Match.orElse(() => substitutePlaceholder(progress, placeholder)));
530
+ }
531
+ function formatStep(progress, part) {
532
+ return Match.value(part).pipe(Match.when((candidate) => PLACEHOLDER_PART.test(candidate), (placeholder) => consumePlaceholder(progress, placeholder)), Match.orElse((literal) => appendLiteral(progress, literal)));
533
+ }
534
+ function formatTemplate(template, args) {
535
+ const progress = template.split(PLACEHOLDER).reduce(formatStep, {
536
+ args,
537
+ cursor: 0,
538
+ text: ""
539
+ });
540
+ const trailing = progress.args.slice(progress.cursor).map((argument) => ` ${inspectValue(argument)}`);
541
+ return progress.text + trailing.join("");
542
+ }
543
+ function isTextTemplate(args) {
544
+ return typeof args[0] === "string";
545
+ }
546
+ function formatArgs(args) {
547
+ return Match.value(args).pipe(Match.when(isTextTemplate, (values) => formatTemplate(values[0], values.slice(1))), Match.orElse((values) => values.map(inspectValue).join(" ")));
617
548
  }
618
549
  function captureSync(args) {
619
550
  capturedConsoleChunks.push(formatArgs(args));
@@ -621,21 +552,39 @@ function captureSync(args) {
621
552
  function captureAssert(condition, args) {
622
553
  if (!condition) capturedConsoleChunks.push(`Assertion failed: ${formatArgs(args)}`);
623
554
  }
624
- function captureCount(label) {
625
- const key = label ?? "default";
626
- const next = (countByLabel.get(key) ?? 0) + 1;
555
+ const DEFAULT_CONSOLE_LABEL = "default";
556
+ function consoleLabel(label) {
557
+ return Option.getOrElse(Option.fromNullishOr(label), () => DEFAULT_CONSOLE_LABEL);
558
+ }
559
+ function incrementCount(key) {
560
+ const next = Option.getOrElse(Option.fromNullishOr(countByLabel.get(key)), () => 0) + 1;
627
561
  countByLabel.set(key, next);
562
+ return next;
563
+ }
564
+ function captureCount(label) {
565
+ const key = consoleLabel(label);
566
+ const next = incrementCount(key);
628
567
  capturedConsoleChunks.push(`${key}: ${next}`);
629
568
  }
569
+ function elapsedMs(started, nowNanos) {
570
+ return Number(nowNanos - started) / 1e6;
571
+ }
630
572
  function captureTimeEnd(label, nowNanos) {
631
- const key = label ?? "default";
573
+ const key = consoleLabel(label);
632
574
  const started = timeByLabel.get(key);
633
575
  if (started !== void 0) {
634
576
  timeByLabel.delete(key);
635
- const diffMs = Number(nowNanos - started) / 1e6;
636
- capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
577
+ capturedConsoleChunks.push(`${key}: ${elapsedMs(started, nowNanos)}ms`);
637
578
  }
638
579
  }
580
+ function captureTimeLog(label, args, clock) {
581
+ const key = consoleLabel(label);
582
+ const started = timeByLabel.get(key);
583
+ if (started === void 0) return;
584
+ const elapsed = elapsedMs(started, clock.monotonicTimeNanosUnsafe());
585
+ const elapsedText = Match.value(args).pipe(Match.when((values) => values.length === 0, () => `${key}: ${elapsed}ms`), Match.orElse((values) => `${key}: ${elapsed}ms ${formatArgs(values)}`));
586
+ capturedConsoleChunks.push(elapsedText);
587
+ }
639
588
  function captureTrace(args) {
640
589
  capturedConsoleChunks.push(`Trace: ${formatArgs(args)}\n${(/* @__PURE__ */ new Error()).stack ?? ""}`);
641
590
  }
@@ -643,7 +592,7 @@ const makeCapturingConsole = (clock) => ({
643
592
  assert: (condition, ...args) => captureAssert(condition, args),
644
593
  clear: () => {},
645
594
  count: (label) => captureCount(label),
646
- countReset: (label) => countByLabel.delete(label ?? "default"),
595
+ countReset: (label) => countByLabel.delete(consoleLabel(label)),
647
596
  debug: (...args) => captureSync(args),
648
597
  dir: (item, _options) => capturedConsoleChunks.push(Formatter.format(item)),
649
598
  dirxml: (item) => capturedConsoleChunks.push(Formatter.format(item)),
@@ -654,19 +603,9 @@ const makeCapturingConsole = (clock) => ({
654
603
  info: (...args) => captureSync(args),
655
604
  log: (...args) => captureSync(args),
656
605
  table: (tabularData) => capturedConsoleChunks.push(Formatter.format(tabularData)),
657
- time: (label) => timeByLabel.set(label ?? "default", clock.monotonicTimeNanosUnsafe()),
606
+ time: (label) => timeByLabel.set(consoleLabel(label), clock.monotonicTimeNanosUnsafe()),
658
607
  timeEnd: (label) => captureTimeEnd(label, clock.monotonicTimeNanosUnsafe()),
659
- timeLog: (label, ...args) => {
660
- const key = label ?? "default";
661
- const started = timeByLabel.get(key);
662
- if (started === void 0) return;
663
- const diffMs = Number(clock.monotonicTimeNanosUnsafe() - started) / 1e6;
664
- if (args.length === 0) {
665
- capturedConsoleChunks.push(`${key}: ${diffMs}ms`);
666
- return;
667
- }
668
- capturedConsoleChunks.push(`${key}: ${diffMs}ms ${formatArgs(args)}`);
669
- },
608
+ timeLog: (label, ...args) => captureTimeLog(label, args, clock),
670
609
  trace: (...args) => captureTrace(args),
671
610
  warn: (...args) => captureSync(args)
672
611
  });
@@ -683,6 +622,506 @@ function resetCapturedConsole() {
683
622
  timeByLabel.clear();
684
623
  }
685
624
  //#endregion
625
+ //#region src/merge-reports.schema.ts
626
+ var MergeReportsFailed = class extends S.TaggedError()("MergeReportsFailed", { reason: S.String }) {};
627
+ const PartMetaSchema = S.Struct({
628
+ package: S.String,
629
+ outcome: S.String
630
+ });
631
+ const StreamMutantLineSchema = S.Struct({
632
+ kind: S.Literal("mutant"),
633
+ id: S.String,
634
+ status: MutantStatusSchema,
635
+ file: S.String,
636
+ location: LocationSchema,
637
+ mutator: S.String,
638
+ replacement: S.NullOr(S.String)
639
+ });
640
+ //#endregion
641
+ //#region src/merge-report-parts.workflow.ts
642
+ const MERGED_SCHEMA_VERSION = "1.7";
643
+ const DEFAULT_THRESHOLDS = {
644
+ high: 80,
645
+ low: 60
646
+ };
647
+ const FAILING_OUTCOME = "failure";
648
+ const PASSING_OUTCOME = "success";
649
+ const PERFECT_MUTATION_SCORE = 100;
650
+ const SURVIVOR_STATUSES = {
651
+ Survived: true,
652
+ NoCoverage: true
653
+ };
654
+ const SCORE_INCOMPLETE = "incomplete";
655
+ const SCORE_ABSENT = "no report";
656
+ const SCORE_UNDEFINED = "n/a";
657
+ const SCORE_PERFECT = "100.00";
658
+ const VERDICT_OK = "✅";
659
+ const VERDICT_FAIL = "❌";
660
+ const VERDICT_WARN = "⚠️";
661
+ const ABSENT_CELLS = [
662
+ "—",
663
+ "—",
664
+ "—",
665
+ "—",
666
+ "—"
667
+ ];
668
+ const ALL_PACKAGES_LABEL$1 = "**all**";
669
+ const SEGMENT_SEPARATORS = /[/\\]/;
670
+ const EMPTY_SEGMENTS = [];
671
+ const EMPTY_PACKAGES = [];
672
+ const EMPTY_TEST_FILES = {};
673
+ const ReportPart = S.Struct({
674
+ label: S.String,
675
+ outcome: S.String,
676
+ incomplete: S.Boolean,
677
+ report: S.optional(MutationTestResultSchema)
678
+ });
679
+ const MergeVerdictRow = S.Struct({
680
+ label: S.String,
681
+ score: S.String,
682
+ cells: S.Array(S.String),
683
+ verdict: S.String
684
+ });
685
+ const MergeSurvivor = S.Struct({
686
+ file: S.String,
687
+ line: S.Finite,
688
+ column: S.Finite,
689
+ status: S.String,
690
+ mutatorName: S.String,
691
+ replacement: S.String
692
+ });
693
+ var MergeReportPartsCommand = class extends S.Class("MergeReportPartsCommand")({
694
+ parts: S.Array(ReportPart),
695
+ expectedPackages: S.optional(S.Array(S.String))
696
+ }) {};
697
+ const MergeReportPartsTypeId = Symbol.for("@systemfsoftware/stryker-js-cli/MergeReportParts");
698
+ var MergedReports = class extends S.TaggedClass()("MergedReports", {
699
+ report: MutationTestResultSchema,
700
+ rows: S.Array(MergeVerdictRow),
701
+ survivors: S.Array(MergeSurvivor)
702
+ }) {
703
+ [MergeReportPartsTypeId] = MergeReportPartsTypeId;
704
+ };
705
+ var NoMergedReports = class extends S.TaggedClass()("NoMergedReports", { rows: S.Array(MergeVerdictRow) }) {
706
+ [MergeReportPartsTypeId] = MergeReportPartsTypeId;
707
+ };
708
+ var DuplicatePackageLabel = class extends S.TaggedError()("DuplicatePackageLabel", { label: S.String }) {};
709
+ var MissingPackages = class extends S.TaggedError()("MissingPackages", { packages: S.Array(S.String) }) {};
710
+ /** The value a present option carries, or the fallback a missing one takes. */
711
+ const orDefault = (present, fallback) => Option.getOrElse(present, () => fallback);
712
+ const thresholdsOf = (part) => orDefault(Option.map(Option.fromUndefinedOr(part), (present) => present.report.thresholds), DEFAULT_THRESHOLDS);
713
+ const hasReport = (part) => part.report !== void 0;
714
+ const scoreFor = (part) => Option.getOrUndefined(Option.map(Option.filter(Option.fromUndefinedOr(part), hasReport), (present) => scoreOf(present.report.files)));
715
+ const outcomeOf = (reports) => Match.value(reports.some((part) => part.outcome === FAILING_OUTCOME)).pipe(Match.when(true, () => FAILING_OUTCOME), Match.when(false, () => PASSING_OUTCOME), Match.exhaustive);
716
+ const normalizeName = (fileName) => fileName.split(SEGMENT_SEPARATORS).filter(Boolean).join("/");
717
+ const commonBasePath = (fileNames) => {
718
+ const directories = fileNames.map((fileName) => fileName.split(SEGMENT_SEPARATORS).slice(0, -1));
719
+ return Arr.reduce(directories, orDefault(Arr.head(directories), EMPTY_SEGMENTS), (left, right) => Arr.takeWhile(left, (segment, index) => right[index] === segment)).join("/");
720
+ };
721
+ const normalizedNames = (input) => {
722
+ const base = commonBasePath(Object.keys(input));
723
+ return Object.fromEntries(Object.entries(input).map(([fileName, value]) => [normalizeName(fileName.slice(base.length)), value]));
724
+ };
725
+ const uniqueId = (label, id) => `${label}_${id}`;
726
+ const uniqueIds = (label, ids) => Option.getOrUndefined(Option.map(Option.fromUndefinedOr(ids), (present) => present.map((id) => uniqueId(label, id))));
727
+ const rewrittenMutant = (label, mutant) => ({
728
+ ...mutant,
729
+ id: uniqueId(label, mutant.id),
730
+ killedBy: uniqueIds(label, mutant.killedBy),
731
+ coveredBy: uniqueIds(label, mutant.coveredBy)
732
+ });
733
+ const withProjectRoot = (report, projectRoot) => Option.match(Option.fromUndefinedOr(projectRoot), {
734
+ onNone: () => report,
735
+ onSome: (root) => ({
736
+ ...report,
737
+ projectRoot: root
738
+ })
739
+ });
740
+ const mergedFiles = (parts) => Object.fromEntries(parts.flatMap((part) => Object.entries(normalizedNames(part.report.files)).map(([fileName, file]) => [`${part.label}/${fileName}`, {
741
+ ...file,
742
+ mutants: file.mutants.map((mutant) => rewrittenMutant(part.label, mutant))
743
+ }])));
744
+ const mergedTestFiles = (parts) => Object.fromEntries(parts.flatMap((part) => Object.entries(normalizedNames(orDefault(Option.fromUndefinedOr(part.report.testFiles), EMPTY_TEST_FILES))).map(([fileName, testFile]) => [`${part.label}/${fileName}`, {
745
+ ...testFile,
746
+ tests: testFile.tests.map((test) => ({
747
+ ...test,
748
+ id: uniqueId(part.label, test.id)
749
+ }))
750
+ }])));
751
+ const withTestFiles = (report, testFiles) => Match.value(Object.keys(testFiles).length > 0).pipe(Match.when(true, () => ({
752
+ ...report,
753
+ testFiles
754
+ })), Match.when(false, () => report), Match.exhaustive);
755
+ const mergeParts = (parts) => {
756
+ const merged = {
757
+ files: mergedFiles(parts),
758
+ schemaVersion: MERGED_SCHEMA_VERSION,
759
+ thresholds: thresholdsOf(parts[0]),
760
+ config: {}
761
+ };
762
+ const projectRoots = parts.flatMap((part) => Option.toArray(Option.fromUndefinedOr(part.report.projectRoot)));
763
+ return withProjectRoot(withTestFiles(merged, mergedTestFiles(parts)), commonProjectRoot(projectRoots));
764
+ };
765
+ const commonProjectRoot = (roots) => Option.getOrUndefined(Option.map(Option.liftPredicate(roots, Arr.isReadonlyArrayNonEmpty), commonBasePath));
766
+ const mutationScoreOf = (totalDetected, totalValid) => Match.value(totalValid === 0).pipe(Match.when(true, () => NaN), Match.when(false, () => totalDetected / totalValid * 100), Match.exhaustive);
767
+ const scoreOf = (files) => {
768
+ const mutants = Object.values(files).flatMap((file) => file.mutants);
769
+ const count = (status) => mutants.filter((mutant) => mutant.status === status).length;
770
+ const killed = count("Killed");
771
+ const timeout = count("Timeout");
772
+ const survived = count("Survived");
773
+ const noCoverage = count("NoCoverage");
774
+ const compileErrors = count("CompileError");
775
+ const totalDetected = timeout + killed;
776
+ const cells = [
777
+ String(killed),
778
+ String(survived),
779
+ String(noCoverage),
780
+ String(timeout),
781
+ String(compileErrors)
782
+ ];
783
+ return {
784
+ mutationScore: mutationScoreOf(totalDetected, survived + noCoverage + totalDetected),
785
+ cells
786
+ };
787
+ };
788
+ const cellsFor = (score) => orDefault(Option.map(Option.fromUndefinedOr(score), (present) => present.cells), ABSENT_CELLS);
789
+ const incompleteVerdict = (outcome) => Match.value(outcome === PASSING_OUTCOME).pipe(Match.when(true, () => VERDICT_WARN), Match.when(false, () => VERDICT_FAIL), Match.exhaustive);
790
+ const passingVerdict = (score, outcome) => Match.value({
791
+ perfect: score.mutationScore === PERFECT_MUTATION_SCORE,
792
+ passing: outcome === PASSING_OUTCOME
793
+ }).pipe(Match.when({
794
+ perfect: true,
795
+ passing: true
796
+ }, () => VERDICT_OK), Match.orElse(() => VERDICT_FAIL));
797
+ const renderedScore = (mutationScore) => Match.value(mutationScore).pipe(Match.when(PERFECT_MUTATION_SCORE, () => SCORE_PERFECT), Match.orElse(() => mutationScore.toFixed(2)));
798
+ const scoredRow = (label, score, outcome) => Match.value(Number.isNaN(score.mutationScore)).pipe(Match.when(true, () => ({
799
+ label,
800
+ score: SCORE_UNDEFINED,
801
+ cells: score.cells,
802
+ verdict: VERDICT_WARN
803
+ })), Match.when(false, () => ({
804
+ label,
805
+ score: renderedScore(score.mutationScore),
806
+ cells: score.cells,
807
+ verdict: passingVerdict(score, outcome)
808
+ })), Match.exhaustive);
809
+ const completeRow = (label, score, outcome) => Option.match(Option.fromUndefinedOr(score), {
810
+ onNone: () => ({
811
+ label,
812
+ score: SCORE_ABSENT,
813
+ cells: ABSENT_CELLS,
814
+ verdict: VERDICT_WARN
815
+ }),
816
+ onSome: (present) => scoredRow(label, present, outcome)
817
+ });
818
+ const verdictOf = (label, score, outcome, incomplete) => Match.value(incomplete).pipe(Match.when(true, () => ({
819
+ label,
820
+ score: SCORE_INCOMPLETE,
821
+ cells: cellsFor(score),
822
+ verdict: incompleteVerdict(outcome)
823
+ })), Match.when(false, () => completeRow(label, score, outcome)), Match.exhaustive);
824
+ /** The label a second part repeats, and `none` when it repeats none a report can name. */
825
+ const repeatedLabel = (parts) => Option.filter(Option.map(Option.fromUndefinedOr(parts.find((part, index) => parts.slice(0, index).some((earlier) => earlier.label === part.label))), (duplicate) => duplicate.label), (label) => label !== "");
826
+ const uniqueParts = (parts) => parts.filter((part, index) => parts.slice(0, index).every((earlier) => earlier.label !== part.label));
827
+ const chooseReports = (parts) => parts.flatMap((part) => Option.toArray(Option.map(Option.fromUndefinedOr(part.report), (report) => ({
828
+ ...part,
829
+ report
830
+ }))));
831
+ const byFileThenLine = (left, right) => orDefault(Option.liftPredicate(left.file.localeCompare(right.file), (ordered) => ordered !== 0), left.line - right.line);
832
+ const survivorsOf = (report) => Object.entries(report.files).flatMap(([file, fileResult]) => fileResult.mutants.filter((mutant) => SURVIVOR_STATUSES[mutant.status] === true).map((mutant) => ({
833
+ file,
834
+ line: mutant.location.start.line,
835
+ column: mutant.location.start.column,
836
+ status: mutant.status,
837
+ mutatorName: mutant.mutatorName,
838
+ replacement: orDefault(Option.fromNullishOr(mutant.replacement), "")
839
+ }))).sort(byFileThenLine);
840
+ const rowOf = (label, part) => Option.match(Option.fromUndefinedOr(part), {
841
+ onNone: () => verdictOf(label, void 0, void 0, false),
842
+ onSome: (present) => verdictOf(label, scoreFor(present), present.outcome, present.incomplete)
843
+ });
844
+ const packageRows = (parts, expected) => {
845
+ const absent = expected.filter((name) => !parts.some((part) => part.label === name));
846
+ return [...parts.map((part) => part.label), ...absent].sort().map((label) => rowOf(label, parts.find((candidate) => candidate.label === label)));
847
+ };
848
+ const allPackagesRow = (reports, merged) => verdictOf(ALL_PACKAGES_LABEL$1, scoreOf(merged.files), outcomeOf(reports), reports.some((part) => part.incomplete));
849
+ const mergedReports = (reports, rows) => {
850
+ const report = mergeParts(reports);
851
+ return MergedReports.make({
852
+ report,
853
+ rows: [allPackagesRow(reports, report), ...rows],
854
+ survivors: survivorsOf(report)
855
+ });
856
+ };
857
+ const mergedOutcome = (reports, rows) => Match.value(reports.length === 0).pipe(Match.when(true, () => Result.succeed(NoMergedReports.make({ rows }))), Match.when(false, () => Result.succeed(mergedReports(reports, rows))), Match.exhaustive);
858
+ const decideMerge = (command) => {
859
+ const parts = uniqueParts(command.parts);
860
+ const expected = orDefault(Option.fromNullishOr(command.expectedPackages), EMPTY_PACKAGES);
861
+ const rows = packageRows(parts, expected);
862
+ return Match.value(parts.length === 0).pipe(Match.when(true, () => Result.fail(MissingPackages.make({ packages: [...expected] }))), Match.when(false, () => mergedOutcome(chooseReports(parts), rows)), Match.exhaustive);
863
+ };
864
+ const DuplicateLabelFound = S.TaggedStruct("DuplicateLabelFound", { label: S.String });
865
+ const LabelsDistinct = S.TaggedStruct("LabelsDistinct", {});
866
+ S.Union([DuplicateLabelFound, LabelsDistinct]);
867
+ const labelCheckOf = (parts) => Option.match(repeatedLabel(parts), {
868
+ onNone: () => LabelsDistinct.make({}),
869
+ onSome: (label) => DuplicateLabelFound.make({ label })
870
+ });
871
+ const decide = (command) => Match.value(labelCheckOf(command.parts)).pipe(Match.tag("DuplicateLabelFound", (found) => Result.fail(DuplicatePackageLabel.make({ label: found.label }))), Match.tag("LabelsDistinct", () => decideMerge(command)), Match.exhaustive);
872
+ const mergeReportParts = Workflow.make(MergeReportPartsCommand, decide);
873
+ //#endregion
874
+ //#region src/MergeReports.ts
875
+ const PART_MARKER_FILE = "mutation-part.json";
876
+ const PART_REPORT_FILE = "mutation-report.json";
877
+ const PART_STREAM_FILE = "mutation-stream.jsonl";
878
+ const MERGED_REPORT_FILE = "mutation-report.json";
879
+ const MERGED_HTML_FILE = "mutation-report.html";
880
+ const MERGED_SUMMARY_FILE = "summary.md";
881
+ const STREAM_REPORT_THRESHOLDS = {
882
+ high: 100,
883
+ low: 80
884
+ };
885
+ const SURVIVOR_CAP = 100;
886
+ const ALL_PACKAGES_LABEL = "**all**";
887
+ const STEP_SUMMARY_VARIABLE = "GITHUB_STEP_SUMMARY";
888
+ /** The value when the condition holds, the fallback otherwise. */
889
+ const whenHolds = (condition, value, fallback) => Option.getOrElse(Option.filter(Option.some(value), () => condition), () => fallback);
890
+ const failMerge = (reason) => Effect.gen(function* () {
891
+ yield* Console.error(`stryker merge-reports: ${reason}`);
892
+ return yield* Effect.fail(MergeReportsFailed.make({ reason }));
893
+ });
894
+ const readOptional = (file) => Effect.gen(function* () {
895
+ return yield* (yield* FileSystem.FileSystem).readFileString(file).pipe(Effect.orElseSucceed(() => void 0));
896
+ });
897
+ const streamMutant = (line) => {
898
+ const mutant = {
899
+ id: line.id,
900
+ mutatorName: line.mutator,
901
+ status: line.status,
902
+ location: line.location
903
+ };
904
+ if (line.replacement === null) return mutant;
905
+ return {
906
+ ...mutant,
907
+ replacement: line.replacement
908
+ };
909
+ };
910
+ const decodeStreamLines = (text) => text.split("\n").map((raw) => raw.trim()).filter((line) => line.length > 0).flatMap((line) => Option.toArray(S.decodeUnknownOption(S.fromJsonString(StreamMutantLineSchema))(line)));
911
+ const groupStreamMutant = (groups, value) => {
912
+ const mutants = Option.getOrElse(Option.fromNullishOr(groups[value.file]), () => []);
913
+ mutants.push(streamMutant(value));
914
+ groups[value.file] = mutants;
915
+ return groups;
916
+ };
917
+ const groupedMutants = (text) => decodeStreamLines(text).reduce(groupStreamMutant, {});
918
+ const fileResults = (grouped) => Object.fromEntries(Object.entries(grouped).map(([file, mutants]) => [file, {
919
+ language: "javascript",
920
+ source: "",
921
+ mutants
922
+ }]));
923
+ const streamReport = (text) => Option.getOrUndefined(Option.map(Option.filter(Option.some(groupedMutants(text)), (grouped) => Object.keys(grouped).length > 0), (grouped) => ({
924
+ schemaVersion: "1.0",
925
+ thresholds: STREAM_REPORT_THRESHOLDS,
926
+ files: fileResults(grouped)
927
+ })));
928
+ const listDirectory = (dir) => Effect.gen(function* () {
929
+ return yield* (yield* FileSystem.FileSystem).readDirectory(dir).pipe(Effect.orElseSucceed(() => []));
930
+ });
931
+ const isDirectory = (full) => Effect.gen(function* () {
932
+ const info = yield* (yield* FileSystem.FileSystem).stat(full).pipe(Effect.orElseSucceed(() => void 0));
933
+ return Option.match(Option.fromUndefinedOr(info), {
934
+ onNone: () => false,
935
+ onSome: (entry) => entry.type === "Directory"
936
+ });
937
+ });
938
+ const childPartDirs = (dir, name) => Effect.gen(function* () {
939
+ const full = (yield* Path.Path).join(dir, name);
940
+ if (yield* isDirectory(full)) return yield* findPartDirs(full);
941
+ return whenHolds(name === PART_MARKER_FILE, [dir], []);
942
+ });
943
+ const findPartDirs = (dir) => Effect.gen(function* () {
944
+ const names = [...yield* listDirectory(dir)].sort();
945
+ return (yield* Effect.forEach(names, (name) => childPartDirs(dir, name))).flat();
946
+ });
947
+ const partBase = (meta) => ({
948
+ label: meta.package,
949
+ outcome: meta.outcome,
950
+ incomplete: false
951
+ });
952
+ const partFromReportText = (dir, base, text) => {
953
+ const report = S.decodeUnknownOption(S.fromJsonString(MutationTestResultSchema))(text);
954
+ return Option.match(report, {
955
+ onNone: () => ({
956
+ dir,
957
+ part: Option.some(base),
958
+ unreadable: true
959
+ }),
960
+ onSome: (value) => ({
961
+ dir,
962
+ part: Option.some({
963
+ ...base,
964
+ report: value
965
+ }),
966
+ unreadable: false
967
+ })
968
+ });
969
+ };
970
+ const readPartStream = (dir, base) => Effect.gen(function* () {
971
+ const path = yield* Path.Path;
972
+ const text = yield* readOptional(path.join(dir, PART_STREAM_FILE));
973
+ const report = Option.fromNullishOr(text).pipe(Option.flatMap((streamed) => Option.fromNullishOr(streamReport(streamed))));
974
+ return Option.match(report, {
975
+ onNone: () => ({
976
+ dir,
977
+ part: Option.some(base),
978
+ unreadable: false
979
+ }),
980
+ onSome: (value) => ({
981
+ dir,
982
+ part: Option.some({
983
+ ...base,
984
+ incomplete: true,
985
+ report: value
986
+ }),
987
+ unreadable: false
988
+ })
989
+ });
990
+ });
991
+ const readPartReport = (dir, base) => Effect.gen(function* () {
992
+ const path = yield* Path.Path;
993
+ const text = yield* readOptional(path.join(dir, PART_REPORT_FILE));
994
+ return yield* Option.match(Option.fromUndefinedOr(text), {
995
+ onNone: () => readPartStream(dir, base),
996
+ onSome: (present) => Effect.succeed(partFromReportText(dir, base, present))
997
+ });
998
+ });
999
+ const readPart = (dir) => Effect.gen(function* () {
1000
+ const path = yield* Path.Path;
1001
+ const metaText = yield* readOptional(path.join(dir, PART_MARKER_FILE));
1002
+ const meta = Option.fromNullishOr(metaText).pipe(Option.flatMap((text) => S.decodeUnknownOption(S.fromJsonString(PartMetaSchema))(text)));
1003
+ return yield* Option.match(meta, {
1004
+ onNone: () => Effect.succeed({
1005
+ dir,
1006
+ part: Option.none(),
1007
+ unreadable: false
1008
+ }),
1009
+ onSome: (present) => readPartReport(dir, partBase(present))
1010
+ });
1011
+ });
1012
+ const readParts = (partsDir) => Effect.gen(function* () {
1013
+ const dirs = yield* findPartDirs(partsDir);
1014
+ const reads = yield* Effect.forEach(dirs, readPart);
1015
+ return {
1016
+ parts: reads.flatMap((read) => Option.toArray(read.part)),
1017
+ skipped: reads.filter((read) => Option.isNone(read.part)).map((read) => read.dir),
1018
+ unreadable: reads.filter((read) => read.unreadable).map((read) => read.dir)
1019
+ };
1020
+ });
1021
+ const decodePackageList = (raw) => Option.match(S.decodeUnknownOption(S.fromJsonString(S.Array(S.String)))(raw), {
1022
+ onNone: () => failMerge(`--packages is not a JSON array: ${raw}`),
1023
+ onSome: (packages) => Effect.succeed(packages)
1024
+ });
1025
+ const parsePackages = (raw) => Option.match(Option.filter(Option.fromNullishOr(raw), (text) => text.length > 0), {
1026
+ onNone: () => Effect.succeed(void 0),
1027
+ onSome: decodePackageList
1028
+ });
1029
+ const SUMMARY_TABLE_HEADER = ["| package | score | killed | survived | no cov | timeout | compile err | verdict |", "| --- | --: | --: | --: | --: | --: | --: | :-: |"];
1030
+ const verdictRow = (row) => `| ${row.label} | ${row.score} | ${row.cells.join(" | ")} | ${row.verdict} |`;
1031
+ const survivorLine = (survivor) => `- \`${survivor.file}:${survivor.line}:${survivor.column}\` ${survivor.status} \`${survivor.mutatorName}\` → \`${survivor.replacement}\``;
1032
+ const survivorSection = (survivors) => [
1033
+ "",
1034
+ "### Survivors",
1035
+ "",
1036
+ ...survivors.slice(0, SURVIVOR_CAP).map(survivorLine),
1037
+ ...whenHolds(survivors.length > SURVIVOR_CAP, [`- … and ${survivors.length - SURVIVOR_CAP} more; see mutation-report.html in the run artifact.`], [])
1038
+ ];
1039
+ const warningSection = (skipped) => [
1040
+ "",
1041
+ "### Warnings",
1042
+ "",
1043
+ ...skipped.map((name) => `- \`${name}\`: no readable mutation-part.json`)
1044
+ ];
1045
+ const mergedCountLine = (packageRows) => `Merged ${packageRows.filter((row) => row.score !== "no report").length} of ${packageRows.length} package report(s).`;
1046
+ const summaryMarkdown = (rows, survivors, skipped, unreadableCount) => {
1047
+ const packageRows = rows.filter((row) => row.label !== ALL_PACKAGES_LABEL);
1048
+ return `${[
1049
+ "## Mutation",
1050
+ "",
1051
+ mergedCountLine(packageRows),
1052
+ "",
1053
+ ...SUMMARY_TABLE_HEADER,
1054
+ ...rows.map(verdictRow),
1055
+ ...whenHolds(survivors.length > 0, survivorSection(survivors), []),
1056
+ ...whenHolds(skipped.length > 0, warningSection(skipped), []),
1057
+ ...whenHolds(unreadableCount > 0, ["", `Report exited non-zero: ${unreadableCount} unreadable part(s).`], [])
1058
+ ].join("\n")}\n`;
1059
+ };
1060
+ const writeFile = (file, content, append) => Effect.gen(function* () {
1061
+ const fs = yield* FileSystem.FileSystem;
1062
+ if (append) {
1063
+ yield* fs.writeFileString(file, content, { flag: "a" });
1064
+ return;
1065
+ }
1066
+ yield* fs.writeFileString(file, content);
1067
+ }).pipe(Effect.catchCause(() => failMerge(`cannot write ${file}`)));
1068
+ async function* toStream(events) {
1069
+ yield* events;
1070
+ }
1071
+ const writeHtmlReport = (fileName, report) => Effect.gen(function* () {
1072
+ const decoded = S.decodeUnknownOption(StrykerOptionsSchema)({ htmlReporter: { fileName } });
1073
+ if (Option.isNone(decoded)) return yield* failMerge(`cannot configure the html report at ${fileName}`);
1074
+ const metrics = calculateMetrics(report.files);
1075
+ yield* Effect.promise(() => makeHtmlReporter(decoded.value, {})(toStream([new MutationTestReportReady({
1076
+ report,
1077
+ metrics
1078
+ })])));
1079
+ });
1080
+ const failureReason = (error, partsDir) => Match.value(error).pipe(Match.tag("DuplicatePackageLabel", (duplicate) => `duplicate package ${duplicate.label}`), Match.tag("MissingPackages", (missing) => {
1081
+ if (missing.packages.length === 0) return `no mutation report parts under ${partsDir}`;
1082
+ return `no mutation report parts under ${partsDir} for ${missing.packages.join(", ")}`;
1083
+ }), Match.exhaustive);
1084
+ const survivorList = (decision) => {
1085
+ if (S.is(MergedReports)(decision)) return decision.survivors;
1086
+ return [];
1087
+ };
1088
+ const mergedReport = (decision) => {
1089
+ if (S.is(MergedReports)(decision)) return decision.report;
1090
+ };
1091
+ const ensurePartsDir = (fs, partsDir) => Effect.gen(function* () {
1092
+ if (yield* fs.exists(partsDir).pipe(Effect.orElseSucceed(() => false))) return;
1093
+ return yield* failMerge(`no such parts directory ${partsDir}`);
1094
+ });
1095
+ const decideReports = (parts, expectedPackages, partsDir) => Effect.gen(function* () {
1096
+ const decision = mergeReportParts(MergeReportPartsCommand.make({
1097
+ parts,
1098
+ expectedPackages
1099
+ }));
1100
+ if (Result.isSuccess(decision)) return decision.success;
1101
+ return yield* failMerge(failureReason(decision.failure, partsDir));
1102
+ });
1103
+ const writeReportOutputs = (path, out, report) => Effect.forEach(Option.toArray(Option.fromUndefinedOr(report)), (present) => Effect.gen(function* () {
1104
+ yield* writeFile(path.join(out, MERGED_REPORT_FILE), JSON.stringify(present), false);
1105
+ yield* writeHtmlReport(path.join(out, MERGED_HTML_FILE), present);
1106
+ }), { discard: true });
1107
+ const appendStepSummary = (summary) => Effect.forEach(Option.toArray(Option.filter(Option.fromNullishOr(process.env[STEP_SUMMARY_VARIABLE]), (file) => file.length > 0)), (file) => writeFile(file, summary, true), { discard: true });
1108
+ const runMergeReports = (request) => Effect.gen(function* () {
1109
+ const fs = yield* FileSystem.FileSystem;
1110
+ const path = yield* Path.Path;
1111
+ const out = request.out;
1112
+ const expectedPackages = yield* parsePackages(request.packages);
1113
+ yield* ensurePartsDir(fs, request.parts);
1114
+ const { parts, skipped, unreadable } = yield* readParts(request.parts);
1115
+ const decision = yield* decideReports(parts, expectedPackages, request.parts);
1116
+ yield* fs.makeDirectory(out, { recursive: true }).pipe(Effect.catchCause(() => failMerge(`cannot create ${out}`)));
1117
+ yield* writeReportOutputs(path, out, mergedReport(decision));
1118
+ const summary = summaryMarkdown(decision.rows, survivorList(decision), skipped, unreadable.length);
1119
+ yield* writeFile(path.join(out, MERGED_SUMMARY_FILE), summary, false);
1120
+ yield* appendStepSummary(summary);
1121
+ yield* Console.log(summary);
1122
+ yield* Effect.forEach(whenHolds(unreadable.length > 0, [`${unreadable.length} unreadable part(s): ${unreadable.join(", ")}`], []), failMerge, { discard: true });
1123
+ });
1124
+ //#endregion
686
1125
  //#region src/resolve-output-mode.workflow.ts
687
1126
  const TOOL_VARIABLES$1 = ["CLAUDECODE", "CODEX_SANDBOX"];
688
1127
  var ResolveModeCommand = class extends S.TaggedClass()("ResolveModeCommand", {
@@ -721,62 +1160,62 @@ var MachineOutput = class extends S.TaggedClass()("MachineOutput", {
721
1160
  [ResolveModeTypeId] = ResolveModeTypeId;
722
1161
  };
723
1162
  const CONFLICT_EXPECTED = "the \"--format text\" and \"--json\" flags are mutually exclusive — use one or the other";
724
- function r4(command) {
725
- if (command.text === true && command.json === true) return Result.fail(ModeConflictError.make({
726
- option: "json",
727
- value: "text",
728
- expected: CONFLICT_EXPECTED
729
- }));
730
- if (command.text === true) return Result.succeed(HumanOutput.make({
731
- signal: "flag",
732
- stdoutIsTTY: command.stdoutIsTTY
733
- }));
734
- if (command.json === true) return Result.succeed(MachineOutput.make({
735
- signal: "flag",
736
- stdoutIsTTY: command.stdoutIsTTY
737
- }));
738
- if (command.envMode !== void 0 && command.envMode.length > 0) {
739
- if (command.envMode === "machine") return Result.succeed(MachineOutput.make({
740
- signal: "env",
741
- stdoutIsTTY: command.stdoutIsTTY
742
- }));
743
- return Result.succeed(HumanOutput.make({
744
- signal: "env",
745
- stdoutIsTTY: command.stdoutIsTTY
746
- }));
747
- }
748
- if (!command.stdoutIsTTY) return Result.succeed(MachineOutput.make({
749
- signal: "tty",
750
- stdoutIsTTY: false
751
- }));
752
- if (command.agent !== void 0 && command.agent.length > 0) return Result.succeed(MachineOutput.make({
1163
+ const configured = (value) => Option.filter(Option.fromNullishOr(value), (text) => text.length > 0);
1164
+ const modeFromEnv = (envMode, stdoutIsTTY) => Match.value(envMode).pipe(Match.when("machine", () => MachineOutput.make({
1165
+ signal: "env",
1166
+ stdoutIsTTY
1167
+ })), Match.orElse(() => HumanOutput.make({
1168
+ signal: "env",
1169
+ stdoutIsTTY
1170
+ })));
1171
+ const modeFromAgent = (command) => Option.match(configured(command.agent), {
1172
+ onNone: () => modeFromTools(command),
1173
+ onSome: () => MachineOutput.make({
753
1174
  signal: "agent",
754
1175
  stdoutIsTTY: true
755
- }));
756
- const toolVars = command.toolVars ?? {};
757
- for (const variable of TOOL_VARIABLES$1) {
758
- const value = toolVars[variable];
759
- if (typeof value === "string" && value.length > 0) return Result.succeed(MachineOutput.make({
760
- signal: "tool",
761
- stdoutIsTTY: true
762
- }));
763
- }
764
- return Result.succeed(HumanOutput.make({
765
- signal: "tty",
766
- stdoutIsTTY: true
767
- }));
768
- }
769
- function modeDecision(command) {
770
- return r4(command);
771
- }
772
- const resolveOutputMode = Workflow.make(ResolveModeCommand, modeDecision);
1176
+ })
1177
+ });
1178
+ const modeFromTools = (command) => Match.value(anyToolVariableSet(command.toolVars)).pipe(Match.when(true, () => MachineOutput.make({
1179
+ signal: "tool",
1180
+ stdoutIsTTY: true
1181
+ })), Match.orElse(() => HumanOutput.make({
1182
+ signal: "tty",
1183
+ stdoutIsTTY: true
1184
+ })));
1185
+ const anyToolVariableSet = (toolVars) => Option.match(Option.fromNullishOr(toolVars), {
1186
+ onNone: () => false,
1187
+ onSome: (named) => TOOL_VARIABLES$1.some((variable) => Option.isSome(configured(named[variable])))
1188
+ });
1189
+ const modeBelowEnv = (command) => Match.value(command.stdoutIsTTY).pipe(Match.when(false, () => MachineOutput.make({
1190
+ signal: "tty",
1191
+ stdoutIsTTY: false
1192
+ })), Match.orElse(() => modeFromAgent(command)));
1193
+ const modeFromEnvironment = (command) => Option.match(configured(command.envMode), {
1194
+ onNone: () => modeBelowEnv(command),
1195
+ onSome: (envMode) => modeFromEnv(envMode, command.stdoutIsTTY)
1196
+ });
1197
+ const decideMode = (command) => Match.value(command).pipe(Match.when({
1198
+ text: true,
1199
+ json: true
1200
+ }, () => Result.fail(ModeConflictError.make({
1201
+ option: "json",
1202
+ value: "text",
1203
+ expected: CONFLICT_EXPECTED
1204
+ }))), Match.when({ text: true }, (texted) => Result.succeed(HumanOutput.make({
1205
+ signal: "flag",
1206
+ stdoutIsTTY: texted.stdoutIsTTY
1207
+ }))), Match.when({ json: true }, (jsoned) => Result.succeed(MachineOutput.make({
1208
+ signal: "flag",
1209
+ stdoutIsTTY: jsoned.stdoutIsTTY
1210
+ }))), Match.orElse((rest) => Result.succeed(modeFromEnvironment(rest))));
1211
+ const resolveOutputMode = Workflow.make(ResolveModeCommand, decideMode);
773
1212
  //#endregion
774
1213
  //#region src/Output.ts
775
1214
  const TICK_INTERVAL_MS = 1e4;
776
1215
  var RunEventStreamPortTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/RunEventStreamPortTag") {};
777
1216
  const RunEventStreamPort = RunEventStreamPortTag;
778
- 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));
779
- 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);
1217
+ const isTerminalEvent = (event) => Match.value(event).pipe(Match.tag("verdict", () => true), Match.tag("error", () => true), Match.tag("help", () => true), Match.orElse(() => false));
1218
+ 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.exhaustive);
780
1219
  const toWireLine = (event) => {
781
1220
  const fields = Object.fromEntries(Object.entries(event).filter(([key]) => key !== "_tag"));
782
1221
  return JSON.stringify({
@@ -813,8 +1252,7 @@ const makeRunEventStream = (stdio, resolved, drainFramed = drainOf.bind(null, st
813
1252
  progress: {
814
1253
  completed: 0,
815
1254
  total: null
816
- },
817
- findingsPrinted: 0
1255
+ }
818
1256
  };
819
1257
  const queueStream = Stream.fromQueue(queue).pipe(Stream.tap((event) => Effect.sync(() => {
820
1258
  Match.value(event).pipe(Match.tag("plan", (e) => {
@@ -838,41 +1276,57 @@ const makeRunEventStream = (stdio, resolved, drainFramed = drainOf.bind(null, st
838
1276
  });
839
1277
  })));
840
1278
  let terminalSeen = false;
841
- const drain = drainFramed(Stream.merge(queueStream, tickStream, { haltStrategy: "either" }).pipe(Stream.tap((event) => {
1279
+ const noteTerminal = (event) => {
1280
+ state.terminalWritten = state.terminalWritten || isTerminalEvent(event);
1281
+ };
1282
+ const progressTap = (event) => {
842
1283
  const line = stderrProgressLine(event, state.terminalWritten);
843
- if (isTerminalEvent(event)) state.terminalWritten = true;
844
- if (line === void 0) return Effect.void;
845
- return writeStderr(stdio, line);
846
- })).pipe(Stream.filter((event) => {
847
- if (state.mode !== "machine") return false;
848
- if (terminalSeen) return false;
849
- if (isTerminalEvent(event)) terminalSeen = true;
1284
+ noteTerminal(event);
1285
+ return Option.match(Option.fromUndefinedOr(line), {
1286
+ onNone: () => Effect.void,
1287
+ onSome: (text) => writeStderr(stdio, text)
1288
+ });
1289
+ };
1290
+ const framingOpen = () => state.mode === "machine" && !terminalSeen;
1291
+ const markTerminalFramed = (event) => {
1292
+ terminalSeen = terminalSeen || isTerminalEvent(event);
1293
+ };
1294
+ const frameEvent = (event) => {
1295
+ if (!framingOpen()) return false;
1296
+ markTerminalFramed(event);
850
1297
  return true;
851
- }), Stream.map((event) => `${toWireLine(event)}\n`)));
1298
+ };
1299
+ const drain = drainFramed(Stream.merge(queueStream, tickStream, { haltStrategy: "either" }).pipe(Stream.tap(progressTap)).pipe(Stream.filter(frameEvent), Stream.map((event) => `${toWireLine(event)}\n`)));
852
1300
  let drainFiber = null;
1301
+ const outputOpen = () => state.mode === "machine" && !state.terminalWritten;
1302
+ const offerStarted = () => Effect.gen(function* () {
1303
+ if (state.mode !== "machine") return;
1304
+ yield* Queue.offer(queue, RunStarted.make({
1305
+ schemaVersion: "1.0",
1306
+ runId,
1307
+ mode: state.mode,
1308
+ signal: state.signal
1309
+ }));
1310
+ });
1311
+ const openHeader = () => Effect.gen(function* () {
1312
+ if (state.headerWritten) return;
1313
+ state.headerWritten = true;
1314
+ yield* offerStarted();
1315
+ });
853
1316
  return {
854
1317
  queue,
855
1318
  runId,
856
1319
  startedAt,
857
- isOpen: () => state.mode === "machine" && !state.terminalWritten && drainFiber !== null,
1320
+ isOpen: () => outputOpen() && drainFiber !== null,
858
1321
  ensureOpen: (openResolved) => {
859
1322
  if (state.headerWritten) return;
860
1323
  state.mode = openResolved.mode;
861
1324
  state.signal = openResolved.signal;
862
1325
  },
863
1326
  open: Effect.gen(function* () {
864
- if (drainFiber === null) {
865
- if (!state.headerWritten) {
866
- state.headerWritten = true;
867
- if (state.mode === "machine") yield* Queue.offer(queue, RunStarted.make({
868
- schemaVersion: "1.0",
869
- runId,
870
- mode: state.mode,
871
- signal: state.signal
872
- }));
873
- }
874
- drainFiber = yield* Effect.forkDetach(drain);
875
- }
1327
+ if (drainFiber !== null) return;
1328
+ yield* openHeader();
1329
+ drainFiber = yield* Effect.forkDetach(drain);
876
1330
  }),
877
1331
  closeAndDrain: Effect.gen(function* () {
878
1332
  state.terminalWritten = true;
@@ -892,74 +1346,33 @@ const decisionToResolvedMode = (decision) => Match.value(decision).pipe(Match.ta
892
1346
  signal: machine.signal,
893
1347
  stdoutIsTTY: machine.stdoutIsTTY
894
1348
  })), Match.exhaustive);
1349
+ const definedToolVars = (vars) => Object.fromEntries(Object.entries(vars ?? {}).filter((entry) => entry[1] !== void 0));
1350
+ const commandFor = (input) => ResolveModeCommand.make({
1351
+ stdoutIsTTY: input.stdoutIsTTY,
1352
+ text: input.text,
1353
+ json: input.json,
1354
+ envMode: input.envMode,
1355
+ agent: input.agent,
1356
+ toolVars: definedToolVars(input.toolVars)
1357
+ });
895
1358
  function isColorEnabled(resolved, noColor) {
896
- if (resolved.mode !== "human") return false;
897
- if (noColor === void 0) return true;
898
- if (noColor.length === 0) return true;
899
- return false;
1359
+ const requested = Option.exists(Option.fromUndefinedOr(noColor), (value) => value.length > 0);
1360
+ return resolved.mode === "human" && !requested;
900
1361
  }
901
1362
  var OutputModeProbeTag = class extends Context.Service()("@systemfsoftware/stryker-js-cli/Output/OutputModeProbeTag") {};
902
1363
  const OutputModeProbe = OutputModeProbeTag;
1364
+ const envToolVars = () => definedToolVars(Object.fromEntries(TOOL_VARIABLES.map((variable) => [variable, process.env[variable]])));
1365
+ const probeInput = (command) => ({
1366
+ stdoutIsTTY: process.stdout.isTTY === true,
1367
+ text: command.text,
1368
+ json: command.json,
1369
+ envMode: process.env["STRYKER_MODE"],
1370
+ agent: process.env["AGENT"],
1371
+ toolVars: envToolVars()
1372
+ });
903
1373
  const outputModeProbeCell = Cell.layer({
904
- read: (command) => Effect.succeed((() => {
905
- const toolVarsRecord = {};
906
- for (const variable of TOOL_VARIABLES) {
907
- const value = process.env[variable];
908
- if (value !== void 0) toolVarsRecord[variable] = value;
909
- }
910
- const envMode = process.env["STRYKER_MODE"];
911
- const agent = process.env["AGENT"];
912
- let result = { stdoutIsTTY: process.stdout.isTTY === true };
913
- if (Object.keys(toolVarsRecord).length > 0) result = {
914
- ...result,
915
- toolVars: toolVarsRecord
916
- };
917
- if (envMode !== void 0) result = {
918
- ...result,
919
- envMode
920
- };
921
- if (agent !== void 0) result = {
922
- ...result,
923
- agent
924
- };
925
- if (command.text !== void 0) result = {
926
- ...result,
927
- text: command.text
928
- };
929
- if (command.json !== void 0) result = {
930
- ...result,
931
- json: command.json
932
- };
933
- return result;
934
- })()),
935
- decode: (raw) => Result.succeed((() => {
936
- const filteredToolVars = {};
937
- if (raw.toolVars !== void 0) {
938
- for (const [key, value] of Object.entries(raw.toolVars)) if (value !== void 0) filteredToolVars[key] = value;
939
- }
940
- let commandInput = { stdoutIsTTY: raw.stdoutIsTTY };
941
- if (raw.text !== void 0) commandInput = {
942
- ...commandInput,
943
- text: raw.text
944
- };
945
- if (raw.json !== void 0) commandInput = {
946
- ...commandInput,
947
- json: raw.json
948
- };
949
- if (raw.envMode !== void 0) commandInput = {
950
- ...commandInput,
951
- envMode: raw.envMode
952
- };
953
- if (raw.agent !== void 0) commandInput = {
954
- ...commandInput,
955
- agent: raw.agent
956
- };
957
- if (Object.keys(filteredToolVars).length > 0) commandInput = {
958
- ...commandInput,
959
- toolVars: filteredToolVars
960
- };
961
- return ResolveModeCommand.make(commandInput);
962
- })()),
1374
+ read: (command) => Effect.succeed(probeInput(command)),
1375
+ decode: (raw) => Result.succeed(commandFor(raw)),
963
1376
  decide: resolveOutputMode,
964
1377
  encode: (outcome) => Result.map(outcome, decisionToResolvedMode),
965
1378
  write: (outcome) => Result.match(outcome, {
@@ -1007,29 +1420,24 @@ function offerFailureEnvelope(stream, failed, captured) {
1007
1420
  remediation: envelope.remediation
1008
1421
  }));
1009
1422
  }
1423
+ const emitHelpEnvelope = (stream, help) => Queue.offer(stream.queue, HelpRendered.make({
1424
+ schemaVersion: "1.0",
1425
+ code: 0,
1426
+ help
1427
+ }));
1428
+ const helpPayload = (ok, captured) => Option.filter(Option.some(captured), () => ok.help || captured.length > 0);
1429
+ const emitNullScoreVerdictWhenOpen = (stream, mode, basePath, pathService) => Effect.gen(function* () {
1430
+ if (!stream.isOpen()) return;
1431
+ yield* emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
1432
+ });
1010
1433
  function emitMachineModeOutput(stream, mode, outcome, basePath, pathService) {
1011
1434
  return Effect.gen(function* () {
1012
1435
  const captured = readCapturedConsole();
1013
1436
  if (Result.isSuccess(outcome)) {
1014
1437
  const decision = outcome.success;
1015
- return yield* Match.value(decision).pipe(Match.tag("RunOk", (ok) => Effect.gen(function* () {
1016
- if (ok.help) {
1017
- yield* Queue.offer(stream.queue, HelpRendered.make({
1018
- schemaVersion: "1.0",
1019
- code: 0,
1020
- help: captured
1021
- }));
1022
- return;
1023
- }
1024
- if (captured.length > 0) {
1025
- yield* Queue.offer(stream.queue, HelpRendered.make({
1026
- schemaVersion: "1.0",
1027
- code: 0,
1028
- help: captured
1029
- }));
1030
- return;
1031
- }
1032
- if (stream.isOpen()) yield* emitNullScoreVerdict(stream, mode, (yield* defaultOptions).thresholds, {}, basePath, pathService);
1438
+ return yield* Match.value(decision).pipe(Match.tag("RunOk", (ok) => Option.match(helpPayload(ok, captured), {
1439
+ onSome: (help) => emitHelpEnvelope(stream, help),
1440
+ onNone: () => emitNullScoreVerdictWhenOpen(stream, mode, basePath, pathService)
1033
1441
  })), Match.tag("RunParseFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunSurvivorsRejected", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunConfigFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.tag("RunFailed", (failed) => offerFailureEnvelope(stream, failed, captured)), Match.exhaustive);
1034
1442
  }
1035
1443
  return yield* offerFailureEnvelope(stream, outcome.failure, captured);
@@ -1066,59 +1474,16 @@ const RunEventStreamFileLive = Layer.effect(RunEventStreamPort, Effect.gen(funct
1066
1474
  }));
1067
1475
  //#endregion
1068
1476
  //#region src/Survivors.ts
1069
- /**
1070
- * Survivors — the survivors-admission capability.
1071
- *
1072
- * The prior-report decoding, source hashing, mutant conversion, and admission
1073
- * pipeline for --survivors runs. Pure admission decision lives in
1074
- * admit-survivors-run.workflow.ts.
1075
- */
1076
1477
  const DEFAULT_SURVIVORS_PRIOR_REPORT = "reports/mutation-report.json";
1077
- /**
1078
- * The mutant shape the admission carries, named once because both the decision's
1079
- * `Admitted` payload and the command's precomputed survivor list are the same shape.
1080
- */
1081
- /**
1082
- * The prior report as a document, decoded at the boundary. Module-internal: consumers
1083
- * get the decode function, not the schema, so the report's wire shape is not a
1084
- * surface commitment and the codec has exactly one caller.
1085
- *
1086
- * `status` is a bare string rather than the closed status set on purpose: the decide only
1087
- * compares it to `'Survived'`, so a report written by a newer engine that added a status
1088
- * must not be refused for carrying one.
1089
- */
1090
- /**
1091
- * Decodes a prior report read from disk. Pure, so it runs in the decode phase, whose
1092
- * `Left` is fatal by construction — it reaches the derived error channel and no write
1093
- * runs. A malformed report therefore never reaches the decider, and nothing here casts
1094
- * a third-party report type.
1095
- */
1096
1478
  const decodePriorReport = S.decodeUnknownResult(PriorReportDocument);
1097
1479
  const { entries: objectEntries, fromEntries: objectFromEntries } = Object;
1098
- /**
1099
- * Thin by design: the digest is the caller's capability, and naming the call
1100
- * keeps every hashing site in the admission path reading the same way.
1101
- */
1480
+ const EMPTY_CONFIG = {};
1102
1481
  function sourceContentHash(content, hash) {
1103
1482
  return hash(content);
1104
1483
  }
1105
- /**
1106
- * The per-file source hashes of the sources a prior report embeds.
1107
- *
1108
- * The current run's side of the comparison is gathered by the shell from disk;
1109
- * this is the recorded side, read back out of the report.
1110
- */
1111
1484
  function priorSourceHashes(priorReport, hashContent) {
1112
1485
  return objectFromEntries(objectEntries(priorReport.files).map(([file, fileResult]) => [file, sourceContentHash(fileResult.source, hashContent)]));
1113
1486
  }
1114
- /**
1115
- * Converts a report mutant (1-based schema location) into the internal mutant
1116
- * shape a run consumes (0-based positions, absolute file name) — the exact
1117
- * inverse of `objectUtils.toSchemaLocation` and the same shift the
1118
- * incremental report reader applies (`project-reader.ts`). Mutants without a
1119
- * replacement fall back to their mutator name, the same convention the
1120
- * incremental differ uses.
1121
- */
1122
1487
  function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
1123
1488
  return Mutant.make({
1124
1489
  id: mutant.id,
@@ -1137,34 +1502,11 @@ function reportMutantToMutant(file, mutant, resolveAbsolutePath) {
1137
1502
  }
1138
1503
  });
1139
1504
  }
1140
- /**
1141
- * The survivors of the prior report: exactly the mutants whose status is
1142
- * `Survived`, converted to the internal mutant shape so a run can re-test
1143
- * them.
1144
- */
1145
1505
  function extractSurvivors(priorReport, resolveAbsolutePath) {
1146
- const survivors = [];
1147
- for (const [file, fileResult] of objectEntries(priorReport.files)) for (const mutant of fileResult.mutants) if (mutant.status === "Survived") survivors.push(reportMutantToMutant(file, mutant, resolveAbsolutePath));
1148
- return survivors;
1506
+ return objectEntries(priorReport.files).flatMap(([file, fileResult]) => fileResult.mutants.filter((mutant) => mutant.status === "Survived").map((mutant) => reportMutantToMutant(file, mutant, resolveAbsolutePath)));
1149
1507
  }
1150
- /**
1151
- * The survivor spans as `file:startLine:startCol-endLine:endCol` mutate
1152
- * ranges: the report's 1-based lines with the internal 0-based columns,
1153
- * relative file names, deduplicated in first-seen order.
1154
- */
1155
1508
  function survivorMutateSpans(survivors, basePath) {
1156
- const spans = [];
1157
- const seen = /* @__PURE__ */ new Set();
1158
- for (const survivor of survivors) {
1159
- const file = toRelativeNormalizedFileName(survivor.fileName, basePath);
1160
- const { start, end } = survivor.location;
1161
- const span = `${file}:${start.line + 1}:${start.column}-${end.line + 1}:${end.column}`;
1162
- if (!seen.has(span)) {
1163
- seen.add(span);
1164
- spans.push(span);
1165
- }
1166
- }
1167
- return spans;
1509
+ return [...new Set(survivors.map((survivor) => `${toRelativeNormalizedFileName(survivor.fileName, basePath)}:${survivor.location.start.line + 1}:${survivor.location.start.column}-${survivor.location.end.line + 1}:${survivor.location.end.column}`))];
1168
1510
  }
1169
1511
  const hashContent = (content) => bytesToHex(sha256(utf8ToBytes(content)));
1170
1512
  const survivorsAdmissionCell = (basePath) => Cell.layer({
@@ -1195,8 +1537,8 @@ const survivorsAdmissionCell = (basePath) => Cell.layer({
1195
1537
  }));
1196
1538
  return Result.map(decodePriorReport(priorReportRaw), (document) => AdmitSurvivorsRunCommand.make({
1197
1539
  priorReport: PriorReportFacts.make({
1198
- config: document.config ?? {},
1199
- frameworkVersion: document.framework?.version
1540
+ config: Option.getOrElse(Option.fromNullishOr(document.config), () => EMPTY_CONFIG),
1541
+ frameworkVersion: Option.getOrUndefined(Option.flatMap(Option.fromNullishOr(document.framework), (framework) => Option.fromNullishOr(framework.version)))
1200
1542
  }),
1201
1543
  currentConfig: resolvedOptions,
1202
1544
  frameworkVersion: strykerVersion,
@@ -1245,11 +1587,7 @@ function readPriorReport(priorReportPath) {
1245
1587
  });
1246
1588
  }
1247
1589
  function priorReportFileKeys(raw) {
1248
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
1249
- if (!("files" in raw)) return [];
1250
- const files = raw.files;
1251
- if (typeof files !== "object" || files === null || Array.isArray(files)) return [];
1252
- return Object.keys(files);
1590
+ return Option.getOrElse(Option.map(Option.flatMap(Option.liftPredicate(raw, Match.record), (document) => Option.liftPredicate(document["files"], Match.record)), (files) => Object.keys(files)), () => []);
1253
1591
  }
1254
1592
  function readSourceFile(file) {
1255
1593
  return Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFileString(file).pipe(Effect.mapError((cause) => ConfigFileUnreadableError.make({
@@ -1266,14 +1604,6 @@ const SIGNAL_NUMBERS = Object.freeze({
1266
1604
  SIGINT: 2,
1267
1605
  SIGTERM: 15
1268
1606
  });
1269
- /**
1270
- * Installs the listeners and returns the reader.
1271
- *
1272
- * The listener records and returns: interrupting the run is the runtime's job,
1273
- * and doing it from here would race the run's own finalizer for the stream.
1274
- * `once` per signal, because a second delivery of the same signal cannot
1275
- * change the answer.
1276
- */
1277
1607
  function observeTerminatingSignal() {
1278
1608
  let observed = null;
1279
1609
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
@@ -1281,269 +1611,23 @@ function observeTerminatingSignal() {
1281
1611
  });
1282
1612
  return () => observed;
1283
1613
  }
1284
- function isObject(value) {
1285
- return typeof value === "object" && value !== null && !Array.isArray(value);
1286
- }
1287
- function stringField(node, key) {
1288
- const value = node[key];
1289
- return (() => {
1290
- if (typeof value === "string") return value;
1291
- })();
1292
- }
1293
- function stringArrayField(node, key) {
1294
- const value = node[key];
1295
- if (!Array.isArray(value)) return [];
1296
- const strings = [];
1297
- for (const item of value) if (typeof item === "string") strings.push(item);
1298
- return strings;
1299
- }
1300
- /** The compiled shapes discriminate on `_tag`; read it once, off the record. */
1301
- function tagOf(node) {
1302
- return node["_tag"];
1303
- }
1304
- function walkParam(param, isOptional, out) {
1305
- if (!isObject(param)) return;
1306
- switch (tagOf(param)) {
1307
- case "Single":
1308
- describeSingle(param, isOptional, out);
1309
- return;
1310
- case "Map":
1311
- case "Transform":
1312
- walkParam(param["param"], isOptional, out);
1313
- return;
1314
- case "Optional":
1315
- walkParam(param["param"], true, out);
1316
- return;
1317
- case "Variadic":
1318
- walkParam(param["param"], isOptional, out);
1319
- return;
1320
- default: return;
1321
- }
1322
- }
1323
- const PRIMITIVE_KIND = {
1324
- Boolean: "boolean",
1325
- Choice: "choice",
1326
- Date: "date",
1327
- FileParse: "file",
1328
- FileSchema: "file",
1329
- FileText: "file",
1330
- Float: "float",
1331
- Integer: "integer",
1332
- KeyValuePair: "key=value",
1333
- None: "none",
1334
- Path: "path",
1335
- Redacted: "redacted",
1336
- String: "text"
1337
- };
1338
- function kindOf(primitive) {
1339
- const tag = stringField(primitive, "_tag");
1340
- return (() => {
1341
- if (tag === void 0) return "unknown";
1342
- return PRIMITIVE_KIND[tag] ?? tag;
1343
- })();
1344
- }
1345
- function choiceValues(primitive) {
1346
- const keys = primitive["choiceKeys"];
1347
- if (!Array.isArray(keys)) return;
1348
- const values = [];
1349
- for (const key of keys) if (typeof key === "string") values.push(key);
1350
- return values;
1351
- }
1352
- const REPORTER_NAMES = [
1353
- "clear-text",
1354
- "progress",
1355
- "html",
1356
- "json",
1357
- "progress-stream"
1358
- ];
1359
- /**
1360
- * v4 option descriptions are stored as `Option.some(string)` on the compiled
1361
- * `Single`; the walker unwraps the option.
1362
- */
1363
- function descriptionOf(single) {
1364
- const description = single["description"];
1365
- if (!isObject(description)) return "";
1366
- switch (tagOf(description)) {
1367
- case "Some": {
1368
- const value = description["value"];
1369
- return (() => {
1370
- if (typeof value === "string") return value;
1371
- return "";
1372
- })();
1373
- }
1374
- default: return "";
1375
- }
1376
- }
1377
- function describeSingle(single, isOptional, out) {
1378
- const name = stringField(single, "name") ?? "";
1379
- const primitive = (() => {
1380
- if (isObject(single["primitiveType"])) return single["primitiveType"];
1381
- return {};
1382
- })();
1383
- const kind = kindOf(primitive);
1384
- const choices = (() => {
1385
- if (name === "reporters") return REPORTER_NAMES;
1386
- if (kind === "choice") return choiceValues(primitive);
1387
- })();
1388
- const description = descriptionOf(single);
1389
- const required = kind !== "boolean" && !isOptional;
1390
- const described = {
1391
- name,
1392
- aliases: stringArrayField(single, "aliases"),
1393
- kind,
1394
- required,
1395
- ...(() => {
1396
- if (choices !== void 0) return { choices };
1397
- return {};
1398
- })(),
1399
- description
1400
- };
1401
- if (single["kind"] === "argument") {
1402
- out.args.push({
1403
- name,
1404
- kind,
1405
- required,
1406
- description
1407
- });
1408
- return;
1409
- }
1410
- out.flags.push(described);
1411
- }
1412
- function walkConfigNode(node, orderedParams, out) {
1413
- if (!isObject(node)) return;
1414
- switch (tagOf(node)) {
1415
- case "Param": {
1416
- const index = node["index"];
1417
- const param = (() => {
1418
- if (typeof index === "number") return orderedParams[index];
1419
- })();
1420
- if (param !== void 0) walkParam(param, false, out);
1421
- return;
1422
- }
1423
- case "Array":
1424
- if (Array.isArray(node["children"])) for (const child of node["children"]) walkConfigNode(child, orderedParams, out);
1425
- return;
1426
- case "Nested":
1427
- if (isObject(node["tree"])) walkConfigTree(node["tree"], orderedParams, out);
1428
- return;
1429
- default: return;
1430
- }
1431
- }
1432
- function walkConfigTree(tree, orderedParams, out) {
1433
- for (const key of Object.keys(tree)) walkConfigNode(tree[key], orderedParams, out);
1434
- }
1435
- function describeCommandNode(node) {
1436
- if (!isObject(node)) return;
1437
- const out = {
1438
- flags: [],
1439
- args: []
1440
- };
1441
- const config = node["config"];
1442
- if (isObject(config) && isObject(config["tree"])) {
1443
- let orderedParams = [];
1444
- const maybeOrdered = config["orderedParams"];
1445
- if (Array.isArray(maybeOrdered)) orderedParams = maybeOrdered;
1446
- walkConfigTree(config["tree"], orderedParams, out);
1447
- }
1448
- const subcommands = [];
1449
- const grouped = node["subcommands"];
1450
- if (Array.isArray(grouped)) for (const group of grouped) {
1451
- if (!isObject(group) || !Array.isArray(group["commands"])) continue;
1452
- for (const child of group["commands"]) {
1453
- const described = describeCommandNode(child);
1454
- if (described !== void 0) subcommands.push(described);
1455
- }
1456
- }
1457
- return {
1458
- name: stringField(node, "name") ?? "",
1459
- description: (() => {
1460
- if (typeof node["description"] === "string") return node["description"];
1461
- return "";
1462
- })(),
1463
- options: out.flags,
1464
- args: out.args,
1465
- subcommands
1466
- };
1467
- }
1468
- function readCoreEntries() {
1469
- return Effect.gen(function* () {
1470
- const path = yield* Path.Path;
1471
- const fs = yield* FileSystem.FileSystem;
1472
- const resolved = import.meta.resolve("@systemfsoftware/stryker-js-engine/package.json");
1473
- const manifestPath = yield* path.fromFileUrl(new URL(resolved));
1474
- const raw = yield* fs.readFileString(manifestPath);
1475
- let parsed;
1476
- try {
1477
- parsed = JSON.parse(raw);
1478
- } catch {
1479
- return [];
1480
- }
1481
- const ExportsSchema = S.Struct({ exports: S.Record(S.String, S.Unknown) });
1482
- const decoded = S.decodeUnknownResult(ExportsSchema)(parsed);
1483
- if (Result.isSuccess(decoded)) return Object.keys(decoded.success.exports).filter((key) => key !== "./package.json");
1484
- return [];
1485
- }).pipe(Effect.catchCause(() => {
1486
- return Effect.succeed([]);
1487
- }));
1488
- }
1489
- function buildLLMSManifest(command, version) {
1490
- return Effect.gen(function* () {
1491
- const root = describeCommandNode(command) ?? {
1492
- name: "",
1493
- description: "",
1494
- options: [],
1495
- args: [],
1496
- subcommands: []
1497
- };
1498
- const entries = yield* readCoreEntries();
1499
- return {
1500
- schemaVersion: "1.0",
1501
- tool: root.name,
1502
- version,
1503
- commands: [root],
1504
- entries
1505
- };
1506
- });
1507
- }
1508
- function emitLLMSManifest(command, version) {
1509
- return Effect.map(buildLLMSManifest(command, version), (manifest) => JSON.stringify(manifest));
1510
- }
1511
1614
  function createSplitter(separator) {
1512
1615
  return (value) => value.split(separator).filter(Boolean);
1513
1616
  }
1514
1617
  const splitOnComma = createSplitter(",");
1515
1618
  const splitOnSpace = createSplitter(" ");
1516
- /**
1517
- * Commander characterization: `always` stays a string, everything else is a
1518
- * boolean where `false`/`0` (case-insensitively) mean `false` — a tri-state a
1519
- * plain boolean or choice would flatten.
1520
- */
1619
+ const CLEAN_TEMP_DIR_DISABLED = ["false", "0"];
1521
1620
  function parseCleanDirOption(value) {
1522
- const v = value.toLocaleLowerCase();
1523
- return (() => {
1524
- if (v === "always") return v;
1525
- return v !== "false" && v !== "0";
1526
- })();
1621
+ const normalized = value.toLocaleLowerCase();
1622
+ return Match.value(normalized).pipe(Match.when("always", () => "always"), Match.orElse(() => !CLEAN_TEMP_DIR_DISABLED.some((disabled) => disabled === normalized)));
1527
1623
  }
1528
- /**
1529
- * Commander characterization: a pure integer is parsed as a number, anything
1530
- * else (e.g. `"50%"`) stays a string.
1531
- */
1532
1624
  function parseConcurrency(value) {
1533
1625
  if (/^\d+$/.test(value)) return parseInt(value, 10);
1534
1626
  return value;
1535
1627
  }
1536
1628
  const optional = (option) => Flag.optional(option);
1537
- /**
1538
- * Commander left an omitted flag out of the parsed options; `deepMerge` treats
1539
- * `undefined` as absent but an explicit `false` would override a config-file
1540
- * `true`. `Flag.optional` yields `Option.none` when the flag is absent and
1541
- * `Option.some(false)` for an explicit `--no-x`, so both map back to
1542
- * `undefined` and leave the config-file default in force (KTD4).
1543
- */
1544
- const absentWhenFalse = (value) => {
1545
- if (Option.isSome(value) && value.value) return true;
1546
- };
1629
+ const absentWhenFalse = (value) => Option.getOrUndefined(Option.filter(value, (present) => present));
1630
+ const isUnknownArgument = (argument) => Option.exists(Option.fromUndefinedOr(argument), (text) => text.startsWith("-"));
1547
1631
  function setLogLevel(target, key, value) {
1548
1632
  const unwrapped = unwrap(value);
1549
1633
  if (unwrapped !== void 0) target[key] = unwrapped;
@@ -1607,7 +1691,11 @@ const runConfig = {
1607
1691
  ...runOptions,
1608
1692
  ...runArgs
1609
1693
  };
1610
- const rootConfig = { llms: Flag.map(optional(Flag.boolean("llms")), absentWhenFalse).pipe(Flag.withDescription("Print the agent-facing command manifest as one JSON object on stdout: every option, alias, kind, default, allowed value set and description, plus the subcommands and positional arguments, walked from the command descriptors.")) };
1694
+ const mergeReportsOptions = {
1695
+ parts: Flag.string("parts").pipe(Flag.withDescription("The directory holding the downloaded mutation report parts (any download layout).")),
1696
+ out: Flag.string("out").pipe(Flag.withDescription("The directory to write the merged report, its html view, and the summary into.")),
1697
+ packages: Flag.string("packages").pipe(Flag.withDescription("A JSON array of the package names the run expected, falling back to the PACKAGES environment variable."), optional)
1698
+ };
1611
1699
  function unwrap(value) {
1612
1700
  if (Option.isOption(value)) return Option.match(value, {
1613
1701
  onNone: () => void 0,
@@ -1619,30 +1707,16 @@ function setIfPresent(target, key, value) {
1619
1707
  const unwrapped = unwrap(value);
1620
1708
  if (unwrapped !== void 0) target[key] = unwrapped;
1621
1709
  }
1622
- /**
1623
- * Builds the full command tree — root plus the `run` subcommand — from the
1624
- * same option/arg records the parser matches against. Each handler leaves the
1625
- * request the executor runs: the run handler writes the parsed options (and
1626
- * the survivors flag, which the admission consumes and the pipeline must not
1627
- * see), the `--llms` handler writes the pre-rendered manifest document, and
1628
- * the bare root writes nothing — `helpRequested` makes the framework render
1629
- * help, which the executor's finalizer turns into the `help` terminal event.
1630
- */
1631
1710
  function makeStrykerCommand(requestRef) {
1632
1711
  const runCommand = Command.make("run", runConfig, (config) => {
1633
1712
  const configFile = Option.getOrUndefined(config.configFile);
1634
- if (configFile !== void 0 && configFile.startsWith("-")) return Console.error(`Received unknown argument: '${configFile}'`).pipe(Effect.andThen(Effect.failSync(() => CliError.UnexpectedArgument.make({ arguments: [configFile] }))));
1713
+ if (isUnknownArgument(configFile)) return Console.error(`Received unknown argument: '${configFile}'`).pipe(Effect.andThen(Effect.failSync(() => CliError.UnexpectedArgument.make({ arguments: [configFile] }))));
1635
1714
  return Ref.set(requestRef, Option.some({
1636
1715
  _tag: "run",
1637
1716
  options: readStrykerOptions(config),
1638
1717
  survivors: config.survivors === true
1639
1718
  }));
1640
1719
  }).pipe(Command.withDescription("Run mutation testing"));
1641
- /**
1642
- * Rebuilds the `PartialStrykerOptions` object commander produced: only options
1643
- * actually given on the command line become keys (KTD4). `survivors` is
1644
- * deliberately not forwarded — the survivor re-run logic (U8) consumes it.
1645
- */
1646
1720
  function readStrykerOptions(config) {
1647
1721
  const options = {};
1648
1722
  setIfPresent(options, "ignorePatterns", config.ignorePatterns);
@@ -1679,25 +1753,18 @@ function makeStrykerCommand(requestRef) {
1679
1753
  if (Option.isSome(config["configFile"])) options["configFile"] = config["configFile"].value;
1680
1754
  return options;
1681
1755
  }
1682
- const strykerCommand = Command.make("stryker", rootConfig, (config) => Effect.gen(function* () {
1683
- if (config.llms === true) {
1684
- const document = {
1685
- _tag: "manifest",
1686
- schemaVersion: "1.0",
1687
- code: 0,
1688
- manifest: yield* emitLLMSManifest(strykerCommand, strykerVersion)
1689
- };
1690
- return yield* Ref.set(requestRef, Option.some({
1691
- _tag: "llms",
1692
- document
1693
- }));
1694
- }
1756
+ const mergeReportsCommand = Command.make("merge-reports", mergeReportsOptions, (config) => Ref.set(requestRef, Option.some({
1757
+ _tag: "merge-reports",
1758
+ parts: config.parts,
1759
+ out: config.out,
1760
+ packages: Option.getOrUndefined(config.packages) ?? process.env["PACKAGES"]
1761
+ }))).pipe(Command.withDescription("Merge per-package mutation reports into one report"));
1762
+ return Command.make("stryker", {}, (_config) => Effect.gen(function* () {
1695
1763
  return yield* Effect.failSync(() => CliError.ShowHelp.make({
1696
1764
  commandPath: ["stryker"],
1697
1765
  errors: []
1698
1766
  }));
1699
- })).pipe(Command.withSubcommands([runCommand]));
1700
- return strykerCommand;
1767
+ })).pipe(Command.withSubcommands([runCommand, mergeReportsCommand]));
1701
1768
  }
1702
1769
  const terminalLayer = Layer.succeed(Terminal.Terminal, Terminal.make({
1703
1770
  columns: Effect.sync(() => process.stdout.columns),
@@ -1748,10 +1815,19 @@ function hostOptionsOf(mode, stream) {
1748
1815
  resolvedMode: mode,
1749
1816
  runStartedAt: stream.startedAt,
1750
1817
  basePath: process.cwd(),
1751
- reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter"), import.meta.resolve("@systemfsoftware/stryker-js-engine/builtin-reporters")],
1818
+ reporterPluginModules: [import.meta.resolve("@systemfsoftware/stryker-js-html-reporter")],
1752
1819
  allowConsoleColors: isColorEnabled(mode, process.env["NO_COLOR"])
1753
1820
  };
1754
1821
  }
1822
+ const isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
1823
+ const progressStreamFileName = (request) => Option.match(request, {
1824
+ onNone: () => DEFAULT_PROGRESS_STREAM_FILE,
1825
+ onSome: (cliRequest) => Match.value(cliRequest).pipe(Match.tag("merge-reports", () => DEFAULT_PROGRESS_STREAM_FILE), Match.tag("run", (runRequest) => Option.getOrElse(Option.filter(Option.fromNullishOr(runRequest.options["progressStreamFile"]), isNonEmptyString), () => DEFAULT_PROGRESS_STREAM_FILE)), Match.exhaustive)
1826
+ });
1827
+ const applyProgressStreamFile = (stream, fileName) => Option.match(Option.fromUndefinedOr(stream.setProgressStreamFile), {
1828
+ onNone: () => Effect.void,
1829
+ onSome: (setFileName) => setFileName(fileName)
1830
+ });
1755
1831
  const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1756
1832
  const stream = yield* createRunEventStream(input.mode);
1757
1833
  const hostOptions = hostOptionsOf(input.mode, stream);
@@ -1764,7 +1840,7 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1764
1840
  process.removeListener("SIGTERM", onSignal);
1765
1841
  if (currentFiber !== null) currentFiber.interruptUnsafe(currentFiber.id);
1766
1842
  };
1767
- const dispatch = (request) => Match.value(request).pipe(Match.tag("run", (runRequest) => (() => {
1843
+ const dispatch = (request) => Match.value(request).pipe(Match.tag("merge-reports", (mergeRequest) => runMergeReports(mergeRequest).pipe(Effect.provide(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)))), Match.tag("run", (runRequest) => (() => {
1768
1844
  if (runRequest.survivors) return Effect.gen(function* () {
1769
1845
  const { admission, resolvedOptions, priorReportPath } = yield* runSurvivorsAdmission(runRequest.options, basePath).pipe(Effect.provide(hostRunLayer(hostOptions)));
1770
1846
  return yield* Match.value(admission).pipe(Match.tag("NoSurvivors", () => emitNullScoreVerdict(stream, input.mode, resolvedOptions.thresholds, resolvedOptions, basePath, pathService)), Match.tag("Admitted", (admitted) => {
@@ -1780,18 +1856,7 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1780
1856
  }), Match.exhaustive);
1781
1857
  });
1782
1858
  return runMutationTestImpl(runRequest.options).pipe(Effect.orDie);
1783
- })()), Match.tag("llms", (llmsRequest) => Effect.gen(function* () {
1784
- stream.ensureOpen({
1785
- mode: "machine",
1786
- signal: "flag",
1787
- stdoutIsTTY: process.stdout.isTTY === true
1788
- });
1789
- yield* Queue.offer(stream.queue, ManifestRendered.make({
1790
- schemaVersion: "1.0",
1791
- code: 0,
1792
- manifest: llmsRequest.document.manifest
1793
- }));
1794
- })), Match.orElse(() => Effect.die("unreachable cli request variant")));
1859
+ })()), Match.exhaustive);
1795
1860
  const program = Effect.acquireUseRelease(Effect.sync(() => {
1796
1861
  currentFiber = Fiber.getCurrent() ?? null;
1797
1862
  process.on("SIGINT", onSignal);
@@ -1799,15 +1864,7 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1799
1864
  }), () => Effect.gen(function* () {
1800
1865
  const parsed = yield* Effect.result(input.program);
1801
1866
  const request = yield* Ref.get(input.requestRef);
1802
- const fileName = Option.match(request, {
1803
- onNone: () => DEFAULT_PROGRESS_STREAM_FILE,
1804
- onSome: (cliRequest) => Match.value(cliRequest).pipe(Match.tag("run", (runRequest) => {
1805
- const fromCli = runRequest.options["progressStreamFile"];
1806
- if (typeof fromCli === "string" && fromCli.length > 0) return fromCli;
1807
- return DEFAULT_PROGRESS_STREAM_FILE;
1808
- }), Match.orElse(() => DEFAULT_PROGRESS_STREAM_FILE))
1809
- });
1810
- if (stream.setProgressStreamFile !== void 0) yield* stream.setProgressStreamFile(fileName);
1867
+ yield* applyProgressStreamFile(stream, progressStreamFileName(request));
1811
1868
  yield* stream.open;
1812
1869
  if (Result.isFailure(parsed)) return yield* Effect.fail(parsed.failure);
1813
1870
  return yield* Option.match(request, {
@@ -1829,14 +1886,18 @@ const runStrykerCli = (input, createRunEventStream) => Effect.gen(function* () {
1829
1886
  //#endregion
1830
1887
  //#region src/main.ts
1831
1888
  const EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER = 1;
1889
+ /** The major version `engines.node` floors the CLI at. */
1890
+ const SUPPORTED_NODE_MAJOR = 20;
1832
1891
  process.title = "stryker";
1833
1892
  const lastSignal = observeTerminatingSignal();
1893
+ /** The numbers a `process.version` names; a component that does not parse stays `NaN`. */
1894
+ const versionNumbers = (version) => version.replace(/^v/, "").split(/[-+]/).slice(0, 1).flatMap((base) => base.split(".")).map((part) => Number.parseInt(part, 10));
1895
+ const componentAt = (numbers, index) => Option.getOrElse(Option.fromUndefinedOr(numbers[index]), () => 0);
1896
+ /** Every reason the CLI refuses the running Node.js, checked against the parsed version numbers. */
1897
+ const NODE_VERSION_REJECTIONS = [(numbers) => numbers.some(Number.isNaN), (numbers) => componentAt(numbers, 0) < SUPPORTED_NODE_MAJOR];
1834
1898
  function isSupportedNodeVersion(version) {
1835
- const [major = 0, minor = 0, patch = 0] = (version.replace(/^v/, "").split(/[-+]/)[0] ?? "").split(".").map((p) => Number.parseInt(p, 10));
1836
- if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) return false;
1837
- if (major !== 20) return major > 20;
1838
- if (minor !== 0) return minor > 0;
1839
- return patch >= 0;
1899
+ const numbers = versionNumbers(version);
1900
+ return !NODE_VERSION_REJECTIONS.some((rejects) => rejects(numbers));
1840
1901
  }
1841
1902
  if (!isSupportedNodeVersion(process.version)) throw new Error(`Node.js version ${process.version} detected. StrykerJS requires version to match ${engines.node}. Please update your Node.js version or visit https://nodejs.org/ for additional instructions`);
1842
1903
  const program = Effect.gen(function* () {
@@ -1844,17 +1905,12 @@ const program = Effect.gen(function* () {
1844
1905
  const runEvents = yield* RunEventStreamPort;
1845
1906
  return yield* strykerCliEffect(process.argv.slice(2), void 0, outputMode.detectMode, runEvents.createRunEventStream, lastSignal);
1846
1907
  }).pipe(Effect.provideService(Logger.LogToStderr, true), Effect.provide(Layer.merge(OutputModeProbeLive, RunEventStreamFileLive).pipe(Layer.provide(Layer.mergeAll(NodeStdio.layer, NodeFileSystem.layer, NodePath.layer)))));
1908
+ const publishedExitCode = (exit) => Option.flatMap(Option.liftPredicate(exit, Exit.isSuccess), (succeeded) => Option.liftPredicate(succeeded.value, (value) => typeof value === "number"));
1909
+ const signalExitCode = (signal) => Option.getOrElse(Option.map(Option.fromNullishOr(signal), (stopped) => 128 + stopped), () => EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER);
1910
+ const teardownExitCode = (exit, signal) => Option.getOrElse(publishedExitCode(exit), () => signalExitCode(signal));
1847
1911
  NodeRuntime.runMain(program, {
1848
1912
  disableErrorReporting: true,
1849
- teardown: (exit, onExit) => {
1850
- if (Exit.isSuccess(exit) && typeof exit.value === "number") {
1851
- onExit(exit.value);
1852
- return;
1853
- }
1854
- const signal = lastSignal();
1855
- if (signal === null) onExit(EXIT_CODE_RUN_NEVER_REACHED_ITS_FINALIZER);
1856
- else onExit(128 + signal);
1857
- }
1913
+ teardown: (exit, onExit) => onExit(teardownExitCode(exit, lastSignal()))
1858
1914
  });
1859
1915
  //#endregion
1860
1916
  export {};