@t09tanaka/test-gates 0.4.1 → 0.4.3

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/cli.js CHANGED
@@ -319,14 +319,17 @@ function parseImports(source) {
319
319
  const clause = match[3];
320
320
  const module = match[5];
321
321
  const braces = /\{([^}]*)\}/.exec(clause);
322
- const names = braces ? braces[1].split(",").map((part) => part.trim()).filter((part) => part !== "" && !/^type\s/.test(part)).map((part) => part.split(/\s+as\s+/)[0].trim()) : [];
322
+ const entries = braces ? braces[1].split(",").map((part) => part.trim()).filter((part) => part !== "") : [];
323
+ const names = entries.filter((part) => !/^type\s/.test(part)).map((part) => part.split(/\s+as\s+/)[0].trim());
323
324
  const outsideBraces = clause.replace(/\{[^}]*\}/, "").replace(/,/g, "").trim();
324
325
  found.push({
325
326
  module,
326
327
  line: lineAt(source, match.index + match[0].lastIndexOf(module)),
327
328
  typeOnly: match[2] !== void 0,
328
329
  names,
329
- whole: outsideBraces !== ""
330
+ whole: outsideBraces !== "",
331
+ ...match[2] === void 0 && outsideBraces === "" && entries.length > 0 && // `type as kind` imports the value named `type`.
332
+ entries.every((part) => /^type\s+(?!as\s)[\w$]/.test(part)) ? { inlineTypes: true } : {}
330
333
  });
331
334
  }
332
335
  for (const match of source.matchAll(SIDE_EFFECT_IMPORT)) {
@@ -341,6 +344,9 @@ function parseImports(source) {
341
344
  }
342
345
  for (const match of source.matchAll(CALL_IMPORT)) {
343
346
  const module = match[2];
347
+ if (match[1] === "`" && module.includes("${")) {
348
+ continue;
349
+ }
344
350
  found.push({
345
351
  module,
346
352
  line: lineAt(source, match.index + match[0].lastIndexOf(module)),
@@ -351,7 +357,7 @@ function parseImports(source) {
351
357
  }
352
358
  return found.sort((a, b) => a.line - b.line);
353
359
  }
354
- var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\(\s*(?!['"`\s)])/g;
360
+ var COMPUTED_CALL_IMPORT = /(?<![.\w$])(?:require|import)\s*\((?!\s*(?:\)|(?:'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"|`(?:[^`\\$]|\\.|\$(?!\{))*`)\s*\)))/g;
355
361
  function findUncheckableImports(source) {
356
362
  return [...source.matchAll(COMPUTED_CALL_IMPORT)].map((match) => lineAt(source, match.index));
357
363
  }
@@ -378,7 +384,11 @@ function resolveSpecifier(specifier, fromFile, aliases) {
378
384
  const segments = [];
379
385
  for (const segment of joined.split("/")) {
380
386
  if (segment === "..") {
381
- segments.pop();
387
+ if (segments.length === 0 || segments[segments.length - 1] === "..") {
388
+ segments.push(segment);
389
+ } else {
390
+ segments.pop();
391
+ }
382
392
  } else if (segment !== "." && segment !== "") {
383
393
  segments.push(segment);
384
394
  }
@@ -423,8 +433,8 @@ function findImporters(gatePath, others, aliases) {
423
433
  if (imported.typeOnly) {
424
434
  return false;
425
435
  }
426
- const resolved = resolveSpecifier(imported.module, other.path, aliases);
427
- return resolved === target || resolved === asDirectory;
436
+ const resolved2 = resolveSpecifier(imported.module, other.path, aliases);
437
+ return resolved2 === target || resolved2 === asDirectory;
428
438
  })
429
439
  ).map((other) => other.path);
430
440
  }
@@ -555,8 +565,8 @@ function scanSpecSource(gatePath, specPath, source, settings) {
555
565
  const gateDirectory = gateModule.replace(/\/index$/, "");
556
566
  for (const match of source.matchAll(MODULE_MOCK)) {
557
567
  const specifier = match[2];
558
- const resolved = resolveSpecifier(specifier, specPath, settings.importAliases);
559
- if (resolved === gateModule || resolved === gateDirectory) {
568
+ const resolved2 = resolveSpecifier(specifier, specPath, settings.importAliases);
569
+ if (resolved2 === gateModule || resolved2 === gateDirectory) {
560
570
  let line = 1;
561
571
  for (let i = 0; i < match.index; i += 1) {
562
572
  if (source[i] === "\n") {
@@ -1024,13 +1034,13 @@ function judgeSelfcheck(input) {
1024
1034
  function selectControls(input) {
1025
1035
  const { gates, mode, only, shard } = input;
1026
1036
  if (only.length > 0) {
1027
- const unknown = only.filter((path13) => !gates.includes(path13));
1037
+ const unknown = only.filter((path15) => !gates.includes(path15));
1028
1038
  if (unknown.length > 0) {
1029
1039
  return {
1030
1040
  error: `--gate ${unknown.join(", ")}: not a gate of test-gates.json (write the path as it is written there)`
1031
1041
  };
1032
1042
  }
1033
- return { selected: gates.filter((path13) => only.includes(path13)) };
1043
+ return { selected: gates.filter((path15) => only.includes(path15)) };
1034
1044
  }
1035
1045
  if (shard !== null) {
1036
1046
  if (shard.total > gates.length) {
@@ -1183,8 +1193,8 @@ function planControls(input) {
1183
1193
  if (silent !== void 0) {
1184
1194
  return everyoneAlone(`the gate run left no note for the spec of ${silent.path}`);
1185
1195
  }
1186
- const partsOf = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
1187
- const short = all.find((gate) => accountedFor(totals[gate.file], partsOf(gate)) === "no");
1196
+ const partsOf3 = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
1197
+ const short = all.find((gate) => accountedFor(totals[gate.file], partsOf3(gate)) === "no");
1188
1198
  if (short !== void 0) {
1189
1199
  return everyoneAlone(
1190
1200
  `the notes of the specs do not add up to what the runner counted for ${short.path}`
@@ -1198,7 +1208,7 @@ function planControls(input) {
1198
1208
  if (all.some((other) => other.path !== gate.path && other.path.includes(gate.path))) {
1199
1209
  return false;
1200
1210
  }
1201
- if (accountedFor(totals[gate.file], partsOf(gate)) !== "yes") {
1211
+ if (accountedFor(totals[gate.file], partsOf3(gate)) !== "yes") {
1202
1212
  return false;
1203
1213
  }
1204
1214
  const touching = notes.filter((note) => {
@@ -1409,207 +1419,630 @@ function runCheck(dir, io2) {
1409
1419
  }
1410
1420
 
1411
1421
  // src/commands/doctor.ts
1412
- import { spawnSync as spawnSync3 } from "child_process";
1413
- import fs9 from "fs";
1414
- import path9 from "path";
1422
+ import { spawnSync as spawnSync5 } from "child_process";
1423
+ import fs11 from "fs";
1424
+ import path11 from "path";
1415
1425
 
1416
- // src/core/reuse-guard.ts
1417
- var RECORD_FORMAT = 2;
1418
- function isStringRecord(value) {
1419
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
1426
+ // src/core/mutation-result.ts
1427
+ var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
1428
+ var SURVIVED = /* @__PURE__ */ new Set(["Survived", "NoCoverage"]);
1429
+ var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
1430
+ function normalizeCode(text) {
1431
+ return text.replace(/\s+/g, " ").trim();
1420
1432
  }
1421
- var isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
1422
- function parseAffected(value) {
1423
- const { tests, gates } = value ?? {};
1424
- if (!isStringList(tests) || typeof gates !== "object" || gates === null || Array.isArray(gates)) {
1425
- return null;
1426
- }
1427
- const parsed = /* @__PURE__ */ Object.create(null);
1428
- for (const [gate, entry] of Object.entries(gates)) {
1429
- const { specs, files } = entry ?? {};
1430
- if (!isStringList(specs) || !isStringList(files)) {
1431
- return null;
1432
- }
1433
- parsed[gate] = { specs, files };
1434
- }
1435
- return { tests, gates: parsed };
1433
+ function normalizePath(file) {
1434
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
1436
1435
  }
1437
- function parseRecord(value) {
1438
- const { format, incremental, snapshot, affected } = value ?? {};
1439
- if (format !== RECORD_FORMAT || typeof incremental !== "string") {
1440
- return null;
1441
- }
1442
- if (typeof snapshot !== "object" || snapshot === null) {
1443
- return null;
1444
- }
1445
- const { mode, environment, args, files } = snapshot;
1446
- if (mode !== "git" && mode !== "walk") {
1447
- return null;
1448
- }
1449
- if (!isStringList(args) || !isStringRecord(environment) || !isStringRecord(files)) {
1436
+ function normalizeNewlines(text) {
1437
+ return text.replace(/\r\n/g, "\n");
1438
+ }
1439
+ function sliceByLocation(source, location) {
1440
+ const lines = source.split("\n");
1441
+ const { start, end } = location;
1442
+ const first = lines[start.line - 1];
1443
+ const last = lines[end.line - 1];
1444
+ if (first === void 0 || last === void 0 || start.column < 1 || end.column < 1 || end.line < start.line || end.line === start.line && end.column < start.column) {
1450
1445
  return null;
1451
1446
  }
1452
- const record = { format, incremental, snapshot: { mode, environment, args, files } };
1453
- if (affected !== void 0) {
1454
- const parsed = parseAffected(affected);
1455
- if (parsed === null) {
1456
- return null;
1457
- }
1458
- record.affected = parsed;
1447
+ if (start.line === end.line) {
1448
+ return first.slice(start.column - 1, end.column - 1);
1459
1449
  }
1460
- return record;
1461
- }
1462
- function listed(names) {
1463
- const shown = names.slice(0, 3).join(", ");
1464
- return names.length > 3 ? `${shown} and ${names.length - 3} more` : shown;
1450
+ return [
1451
+ first.slice(start.column - 1),
1452
+ ...lines.slice(start.line, end.line - 1),
1453
+ last.slice(0, end.column - 1)
1454
+ ].join("\n");
1465
1455
  }
1466
- function differingKeys(before, after) {
1467
- return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => before[key] !== after[key]).sort();
1456
+ function keyOf(mutator, original, replacement) {
1457
+ return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
1468
1458
  }
1469
- function readRecord(input) {
1470
- if (input.record === void 0) {
1471
- return { refused: "there is no record of the run that wrote the incremental file" };
1472
- }
1473
- const record = parseRecord(input.record);
1474
- if (record === null) {
1475
- return { refused: "the record of the last run is not one this version can read" };
1476
- }
1477
- if (record.incremental !== input.incremental) {
1478
- return { refused: "the incremental file is not the one the last recorded run wrote" };
1479
- }
1480
- const before = record.snapshot;
1481
- const now = input.snapshot;
1482
- if (before.mode !== now.mode) {
1483
- return {
1484
- refused: `the files are listed in another way than in the last run (${before.mode}, now ${now.mode})`
1485
- };
1486
- }
1487
- const environment = differingKeys(before.environment, now.environment);
1488
- if (environment.length > 0) {
1489
- const changes = environment.map(
1490
- (key) => `${key} ${before.environment[key] ?? "none"} \u2192 ${now.environment[key] ?? "none"}`
1491
- );
1492
- return { refused: `the tools changed since the last run: ${listed(changes)}` };
1459
+ function locateMutants(source, mutants) {
1460
+ const located = [];
1461
+ const byKey = /* @__PURE__ */ new Map();
1462
+ const outside = [];
1463
+ const sorted2 = [...mutants].sort(
1464
+ (a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
1465
+ );
1466
+ for (const mutant of sorted2) {
1467
+ const original = sliceByLocation(normalizeNewlines(source), mutant.location);
1468
+ if (original === null) {
1469
+ outside.push(mutant);
1470
+ continue;
1471
+ }
1472
+ const replacement = mutant.replacement ?? "";
1473
+ const key = keyOf(mutant.mutatorName, original, replacement);
1474
+ const group = byKey.get(key) ?? [];
1475
+ const entry = { mutant, original, replacement, occurrence: group.length + 1, sameKeyCount: 0 };
1476
+ group.push(entry);
1477
+ byKey.set(key, group);
1478
+ located.push(entry);
1493
1479
  }
1494
- if (JSON.stringify(before.args) !== JSON.stringify(now.args)) {
1495
- return { refused: "the arguments for Stryker are not those of the last run" };
1480
+ for (const group of byKey.values()) {
1481
+ for (const entry of group) {
1482
+ entry.sameKeyCount = group.length;
1483
+ }
1496
1484
  }
1497
- return { record };
1485
+ return { located, byKey, outside };
1498
1486
  }
1499
- function refuseReuse(input) {
1500
- const read2 = readRecord(input);
1501
- if ("refused" in read2) {
1502
- return read2.refused;
1503
- }
1504
- const files = differingKeys(read2.record.snapshot.files, input.snapshot.files);
1505
- if (files.length > 0) {
1506
- return `changed since the last run: ${listed(files)}`;
1487
+ function resolveEntry(entry, byKey) {
1488
+ const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
1489
+ if (entry.occurrence === void 0 && group.length > 1) {
1490
+ return { ambiguous: group.length };
1507
1491
  }
1508
- return null;
1509
- }
1510
- function incrementalOptions(config, args) {
1511
- const options = typeof config === "object" && config !== null ? config : {};
1512
- const valueOf = (name) => {
1513
- let value;
1514
- args.forEach((arg, position) => {
1515
- if (arg === name) {
1516
- value = args[position + 1];
1517
- } else if (arg.startsWith(`${name}=`)) {
1518
- value = arg.slice(name.length + 1);
1519
- }
1520
- });
1521
- return value;
1522
- };
1523
- const fromConfig = (name, fallback) => typeof options[name] === "string" ? options[name] : fallback;
1524
- return {
1525
- incremental: options.incremental === true || args.includes("--incremental"),
1526
- force: options.force === true || args.includes("--force"),
1527
- incrementalFile: valueOf("--incrementalFile") ?? fromConfig("incrementalFile", "reports/stryker-incremental.json"),
1528
- tempDirName: valueOf("--tempDirName") ?? fromConfig("tempDirName", ".stryker-tmp")
1529
- };
1530
- }
1531
- function comparableArgs(args) {
1532
- return args.filter((arg) => arg !== "--force");
1492
+ return { target: group[(entry.occurrence ?? 1) - 1] };
1533
1493
  }
1534
-
1535
- // src/core/affected.ts
1536
- var CONFIG_NAMES = [
1537
- /^package\.json$/,
1538
- /^(?:package-lock|npm-shrinkwrap)\.json$/,
1539
- /^test-gates\.json$/,
1540
- /^[jt]sconfig(?:\..*)?\.json$/,
1541
- /\.config\.[^.]+$/,
1542
- /\.config\.[^.]+\.[^.]+$/,
1543
- /^(?:jest|vitest|vite|stryker|babel)[.-]/,
1544
- /^\./,
1545
- // Declarations need no import to take effect.
1546
- /\.d\.[cm]?ts$/
1547
- ];
1548
- var CONFIG_DIRECTORIES = ["__mocks__", "node_modules"];
1549
- var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc)$/i;
1550
- var PASSIVE = /\.(?:[cm]?[jt]sx?|json|snap|css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico)$/i;
1551
- function kindOf(key, subproject) {
1552
- const inside = subproject === "" || key.startsWith(`${subproject}/`);
1553
- const local = inside && subproject !== "" ? key.slice(subproject.length + 1) : key;
1554
- const segments = local.split("/");
1555
- const name = segments[segments.length - 1];
1556
- if (segments.some((segment) => CONFIG_DIRECTORIES.includes(segment)) || CONFIG_NAMES.some((pattern) => pattern.test(name))) {
1557
- return "config";
1558
- }
1559
- if (DOCUMENT.test(name)) {
1560
- return "document";
1494
+ function expectedTimeoutMutants(gate, source, mutants) {
1495
+ const pointed = /* @__PURE__ */ new Set();
1496
+ if (source === void 0 || !Array.isArray(gate.expectedTimeouts)) {
1497
+ return pointed;
1561
1498
  }
1562
- if (!inside || segments.length === 1 || !PASSIVE.test(name)) {
1563
- return "other";
1499
+ const { byKey, outside } = locateMutants(source, mutants);
1500
+ if (outside.length > 0) {
1501
+ return pointed;
1564
1502
  }
1565
- return "passive";
1566
- }
1567
- function gateDependencies(input) {
1568
- const closureOf = (key) => Object.hasOwn(input.closures, key) ? input.closures[key] : [key];
1569
- const result = /* @__PURE__ */ Object.create(null);
1570
- for (const gate of input.gates) {
1571
- const covering = Object.hasOwn(input.covering, gate.key) ? input.covering[gate.key] : [];
1572
- const specs = /* @__PURE__ */ new Set([...gate.specs, ...covering]);
1573
- for (const test of input.tests) {
1574
- if (closureOf(test).includes(gate.key)) {
1575
- specs.add(test);
1576
- }
1503
+ for (const entry of gate.expectedTimeouts) {
1504
+ if (!isUsableEquivalentMutant(entry)) {
1505
+ continue;
1577
1506
  }
1578
- const files = new Set(closureOf(gate.key));
1579
- for (const spec of specs) {
1580
- closureOf(spec).forEach((file) => files.add(file));
1507
+ const found = resolveEntry(entry, byKey);
1508
+ if ("target" in found && found.target !== void 0) {
1509
+ pointed.add(found.target.mutant);
1581
1510
  }
1582
- result[gate.key] = { specs: [...specs].sort(), files: [...files].sort() };
1583
1511
  }
1584
- return result;
1512
+ return pointed;
1585
1513
  }
1586
- function planAffected(input) {
1587
- const recorded = input.before.affected;
1588
- if (recorded === void 0) {
1589
- return {
1590
- full: "the last run did not write down what each gate depends on (it ran without --affected)"
1591
- };
1592
- }
1593
- const changed = differingKeys(input.before.files, input.now);
1594
- const reached = new Set(input.gates.flatMap((gate) => gate.files));
1595
- Object.values(recorded.gates).forEach((gate) => gate.files.forEach((file) => reached.add(file)));
1596
- const config = new Set(input.config);
1597
- const everywhere = changed.filter((key) => {
1598
- const kind = kindOf(key, input.subproject);
1599
- return config.has(key) || kind === "config" || kind === "other" && !reached.has(key);
1600
- });
1601
- if (everywhere.length > 0) {
1602
- return { full: `changed since the last run, and may affect every gate: ${listed(everywhere)}` };
1514
+ function describeMutant(file, entry) {
1515
+ const { mutant } = entry;
1516
+ return {
1517
+ file,
1518
+ line: mutant.location.start.line,
1519
+ column: mutant.location.start.column,
1520
+ status: mutant.status,
1521
+ mutator: mutant.mutatorName,
1522
+ original: normalizeCode(entry.original),
1523
+ replacement: normalizeCode(entry.replacement),
1524
+ occurrence: entry.occurrence,
1525
+ sameKeyCount: entry.sameKeyCount,
1526
+ allowance: {
1527
+ mutator: mutant.mutatorName,
1528
+ original: normalizeCode(entry.original),
1529
+ replacement: normalizeCode(entry.replacement),
1530
+ ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
1531
+ reason: ""
1532
+ }
1533
+ };
1534
+ }
1535
+ function describeAllowance(entry) {
1536
+ const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
1537
+ return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
1538
+ }
1539
+ function judgeMutationReport(input) {
1540
+ const violations = [];
1541
+ const survivors = [];
1542
+ const unexpectedTimeouts = [];
1543
+ const summary = {
1544
+ total: 0,
1545
+ detected: 0,
1546
+ timeout: 0,
1547
+ allowed: 0,
1548
+ notEvaluable: 0,
1549
+ unallowed: 0,
1550
+ expectedTimeout: 0
1551
+ };
1552
+ const files = /* @__PURE__ */ new Map();
1553
+ for (const [file, result] of Object.entries(input.report.files ?? {})) {
1554
+ files.set(normalizePath(file), result);
1603
1555
  }
1604
- const rerun = [];
1605
- const reused = [];
1606
1556
  for (const gate of input.gates) {
1607
- const last = Object.hasOwn(recorded.gates, gate.path) ? recorded.gates[gate.path] : void 0;
1608
- if (last === void 0) {
1609
- rerun.push({ gate: gate.path, changed: [] });
1557
+ const gatePath = normalizePath(gate.path);
1558
+ const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
1559
+ violations.push(...shapeViolations);
1560
+ violations.push(
1561
+ ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
1562
+ );
1563
+ const result = files.get(gatePath);
1564
+ if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
1565
+ violations.push({
1566
+ file: gate.path,
1567
+ message: "no mutants in the report (the gate was not mutation tested)"
1568
+ });
1610
1569
  continue;
1611
1570
  }
1612
- const files = [.../* @__PURE__ */ new Set([...last.files, ...gate.files])];
1571
+ const diskSource = input.readSource(gate.path);
1572
+ const source = result.source ?? diskSource;
1573
+ if (source === void 0) {
1574
+ violations.push({
1575
+ file: gate.path,
1576
+ message: "source is neither in the report nor readable on disk"
1577
+ });
1578
+ continue;
1579
+ }
1580
+ if (result.source !== void 0 && diskSource !== void 0 && normalizeNewlines(result.source) !== normalizeNewlines(diskSource)) {
1581
+ violations.push({
1582
+ file: gate.path,
1583
+ message: "the report was made from a different version of this file. Run Stryker again"
1584
+ });
1585
+ continue;
1586
+ }
1587
+ const { located, byKey, outside } = locateMutants(source, result.mutants);
1588
+ for (const mutant of outside) {
1589
+ violations.push({
1590
+ file: gate.path,
1591
+ line: mutant.location.start.line,
1592
+ message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
1593
+ });
1594
+ }
1595
+ if (outside.length > 0) {
1596
+ continue;
1597
+ }
1598
+ const claim = (field, entries, noun, accept) => {
1599
+ const claimed = /* @__PURE__ */ new Set();
1600
+ if (!Array.isArray(entries)) {
1601
+ return claimed;
1602
+ }
1603
+ entries.forEach((entry, index) => {
1604
+ if (!isUsableEquivalentMutant(entry)) {
1605
+ return;
1606
+ }
1607
+ const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
1608
+ const found = resolveEntry(entry, byKey);
1609
+ if ("ambiguous" in found) {
1610
+ violations.push({
1611
+ file: MANIFEST_FILE,
1612
+ message: `${at}: matches ${found.ambiguous} mutants in the file. Add "occurrence" (1-based) to pick one`
1613
+ });
1614
+ return;
1615
+ }
1616
+ const target = found.target;
1617
+ const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
1618
+ if (!target || refusal !== null) {
1619
+ violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
1620
+ return;
1621
+ }
1622
+ if (claimed.has(target)) {
1623
+ violations.push({
1624
+ file: MANIFEST_FILE,
1625
+ message: `${at}: another ${noun} already covers this mutant`
1626
+ });
1627
+ return;
1628
+ }
1629
+ claimed.add(target);
1630
+ });
1631
+ return claimed;
1632
+ };
1633
+ const allowed = claim(
1634
+ "equivalentMutants",
1635
+ gate.equivalentMutants,
1636
+ "allowance",
1637
+ (status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
1638
+ );
1639
+ const expected = claim(
1640
+ "expectedTimeouts",
1641
+ gate.expectedTimeouts,
1642
+ "entry",
1643
+ (status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
1644
+ );
1645
+ for (const entry of located) {
1646
+ const { mutant } = entry;
1647
+ summary.total += 1;
1648
+ if (DETECTED.has(mutant.status)) {
1649
+ summary.detected += 1;
1650
+ if (mutant.status === "Timeout") {
1651
+ summary.timeout += 1;
1652
+ if (expected.has(entry)) {
1653
+ summary.expectedTimeout += 1;
1654
+ } else {
1655
+ unexpectedTimeouts.push(describeMutant(gate.path, entry));
1656
+ }
1657
+ }
1658
+ } else if (NOT_EVALUABLE.has(mutant.status)) {
1659
+ summary.notEvaluable += 1;
1660
+ } else if (SURVIVED.has(mutant.status)) {
1661
+ if (allowed.has(entry)) {
1662
+ summary.allowed += 1;
1663
+ continue;
1664
+ }
1665
+ summary.unallowed += 1;
1666
+ survivors.push(describeMutant(gate.path, entry));
1667
+ } else {
1668
+ violations.push({
1669
+ file: gate.path,
1670
+ line: mutant.location.start.line,
1671
+ message: mutant.status === "Ignored" ? `a ${mutant.mutatorName} mutant was ignored. Taking mutants out with Stryker comments or mutator exclusions is not allowed` : `a ${mutant.mutatorName} mutant was not evaluated (status: ${mutant.status})`
1672
+ });
1673
+ }
1674
+ }
1675
+ }
1676
+ if (summary.total === 0) {
1677
+ violations.push({
1678
+ file: MANIFEST_FILE,
1679
+ message: "no mutants were evaluated (the mutation test ran on nothing)"
1680
+ });
1681
+ }
1682
+ const maxTimeouts = input.maxTimeouts ?? 0;
1683
+ if (unexpectedTimeouts.length > maxTimeouts) {
1684
+ violations.push({
1685
+ file: MANIFEST_FILE,
1686
+ message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
1687
+ });
1688
+ }
1689
+ return { violations, survivors, unexpectedTimeouts, summary };
1690
+ }
1691
+ var UNKNOWN_TEST = "not-in-the-first-run:";
1692
+ function sameMutant(a, b) {
1693
+ return a.mutatorName === b.mutatorName && (a.replacement ?? "") === (b.replacement ?? "") && a.location.start.line === b.location.start.line && a.location.start.column === b.location.start.column && a.location.end.line === b.location.end.line && a.location.end.column === b.location.end.column;
1694
+ }
1695
+ function mergeRetry(first, retry, gates) {
1696
+ const outcome = { retried: 0, statuses: {}, missing: 0 };
1697
+ const firstId = translator(first, retry);
1698
+ const files = { ...first.files };
1699
+ const again = retry.files ?? {};
1700
+ for (const named of gates) {
1701
+ const gate = named.path.split("/").filter((part) => part !== ".").join("/");
1702
+ const result = Object.hasOwn(files, gate) ? files[gate] : void 0;
1703
+ if (result?.mutants === void 0) {
1704
+ continue;
1705
+ }
1706
+ const expected = expectedTimeoutMutants(named, result.source, result.mutants);
1707
+ const rerun = Object.hasOwn(again, gate) ? again[gate]?.mutants : void 0;
1708
+ files[gate] = {
1709
+ ...result,
1710
+ mutants: result.mutants.map((mutant) => {
1711
+ if (mutant.status !== "Timeout" || expected.has(mutant)) {
1712
+ return mutant;
1713
+ }
1714
+ outcome.retried += 1;
1715
+ const found = rerun?.find((other) => sameMutant(other, mutant));
1716
+ if (found === void 0) {
1717
+ outcome.missing += 1;
1718
+ return mutant;
1719
+ }
1720
+ outcome.statuses[found.status] = (outcome.statuses[found.status] ?? 0) + 1;
1721
+ return {
1722
+ ...found,
1723
+ ...found.coveredBy === void 0 ? {} : { coveredBy: found.coveredBy.map(firstId) },
1724
+ ...found.killedBy === void 0 ? {} : { killedBy: found.killedBy.map(firstId) }
1725
+ };
1726
+ })
1727
+ };
1728
+ }
1729
+ return { report: { ...first, files }, outcome };
1730
+ }
1731
+ function testKey(file, test) {
1732
+ const start = test.location?.start;
1733
+ return JSON.stringify([file, test.name ?? "", start?.line, start?.column]);
1734
+ }
1735
+ function* keyedTests(report) {
1736
+ for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
1737
+ if (entry.tests !== void 0) {
1738
+ for (const test of entry.tests) {
1739
+ yield { id: test.id, key: testKey(file, test) };
1740
+ }
1741
+ }
1742
+ }
1743
+ }
1744
+ function translator(first, retry) {
1745
+ const firstIds = /* @__PURE__ */ new Map();
1746
+ for (const { id, key } of keyedTests(first)) {
1747
+ firstIds.set(key, firstIds.has(key) ? null : id);
1748
+ }
1749
+ const ids = /* @__PURE__ */ new Map();
1750
+ for (const { id, key } of keyedTests(retry)) {
1751
+ ids.set(id, firstIds.get(key));
1752
+ }
1753
+ return (id) => {
1754
+ const found = ids.get(id);
1755
+ return typeof found === "string" ? found : `${UNKNOWN_TEST}${id}`;
1756
+ };
1757
+ }
1758
+ function formatRetry(outcome) {
1759
+ const statuses = Object.entries(outcome.statuses).sort(([a], [b]) => a.localeCompare(b)).map(([status, count]) => `${count} ${status}`);
1760
+ if (outcome.missing > 0) {
1761
+ statuses.push(`${outcome.missing} not in the report (still Timeout)`);
1762
+ }
1763
+ return `test-gates mutation: ${outcome.retried} mutant(s) that timed out were run again one at a time (--concurrency 1): ${statuses.join(", ")}`;
1764
+ }
1765
+ function formatSurvivor(survivor) {
1766
+ const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
1767
+ return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
1768
+ }
1769
+ function formatSummary(summary) {
1770
+ return `mutants ${summary.total} / detected ${summary.detected} (timeout ${summary.timeout}) / allowed equivalent ${summary.allowed} / not evaluable ${summary.notEvaluable} / unallowed survivors ${summary.unallowed}`;
1771
+ }
1772
+
1773
+ // src/core/reuse-guard.ts
1774
+ var RECORD_FORMAT = 3;
1775
+ function isStringRecord(value) {
1776
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
1777
+ }
1778
+ var isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
1779
+ function parseAffected(value) {
1780
+ const { tests, gates } = value ?? {};
1781
+ if (!isStringList(tests) || typeof gates !== "object" || gates === null || Array.isArray(gates)) {
1782
+ return null;
1783
+ }
1784
+ const parsed = /* @__PURE__ */ Object.create(null);
1785
+ for (const [gate, entry] of Object.entries(gates)) {
1786
+ const { specs, files, blind } = entry ?? {};
1787
+ if (!isStringList(specs) || !isStringList(files)) {
1788
+ return null;
1789
+ }
1790
+ if (blind !== void 0 && typeof blind !== "string") {
1791
+ return null;
1792
+ }
1793
+ parsed[gate] = { specs, files, ...blind === void 0 ? {} : { blind } };
1794
+ }
1795
+ return { tests, gates: parsed };
1796
+ }
1797
+ function parseRecord(value) {
1798
+ const { format, incremental, snapshot, affected } = value ?? {};
1799
+ if (format !== RECORD_FORMAT || typeof incremental !== "string") {
1800
+ return null;
1801
+ }
1802
+ if (typeof snapshot !== "object" || snapshot === null) {
1803
+ return null;
1804
+ }
1805
+ const { mode, environment, args, files } = snapshot;
1806
+ if (mode !== "git" && mode !== "walk") {
1807
+ return null;
1808
+ }
1809
+ if (!isStringList(args) || !isStringRecord(environment) || !isStringRecord(files)) {
1810
+ return null;
1811
+ }
1812
+ const record = { format, incremental, snapshot: { mode, environment, args, files } };
1813
+ if (affected !== void 0) {
1814
+ const parsed = parseAffected(affected);
1815
+ if (parsed === null) {
1816
+ return null;
1817
+ }
1818
+ record.affected = parsed;
1819
+ }
1820
+ return record;
1821
+ }
1822
+ function listed(names) {
1823
+ const shown = names.slice(0, 3).join(", ");
1824
+ return names.length > 3 ? `${shown} and ${names.length - 3} more` : shown;
1825
+ }
1826
+ function differingKeys(before, after) {
1827
+ return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter((key) => before[key] !== after[key]).sort();
1828
+ }
1829
+ function readRecord(input) {
1830
+ if (input.record === void 0) {
1831
+ return { refused: "there is no record of the run that wrote the incremental file" };
1832
+ }
1833
+ const record = parseRecord(input.record);
1834
+ if (record === null) {
1835
+ return { refused: "the record of the last run is not one this version can read" };
1836
+ }
1837
+ if (record.incremental !== input.incremental) {
1838
+ return { refused: "the incremental file is not the one the last recorded run wrote" };
1839
+ }
1840
+ const before = record.snapshot;
1841
+ const now = input.snapshot;
1842
+ if (before.mode !== now.mode) {
1843
+ return {
1844
+ refused: `the files are listed in another way than in the last run (${before.mode}, now ${now.mode})`
1845
+ };
1846
+ }
1847
+ const environment = differingKeys(before.environment, now.environment);
1848
+ if (environment.length > 0) {
1849
+ const changes = environment.map(
1850
+ (key) => `${key} ${before.environment[key] ?? "none"} \u2192 ${now.environment[key] ?? "none"}`
1851
+ );
1852
+ return { refused: `the tools changed since the last run: ${listed(changes)}` };
1853
+ }
1854
+ if (JSON.stringify(before.args) !== JSON.stringify(now.args)) {
1855
+ return { refused: "the arguments for Stryker are not those of the last run" };
1856
+ }
1857
+ return { record };
1858
+ }
1859
+ function refuseReuse(input) {
1860
+ const read2 = readRecord(input);
1861
+ if ("refused" in read2) {
1862
+ return read2.refused;
1863
+ }
1864
+ const files = differingKeys(read2.record.snapshot.files, input.snapshot.files);
1865
+ if (files.length > 0) {
1866
+ return `changed since the last run: ${listed(files)}`;
1867
+ }
1868
+ return null;
1869
+ }
1870
+ function incrementalOptions(config, args) {
1871
+ const options = typeof config === "object" && config !== null ? config : {};
1872
+ const valueOf = (name) => {
1873
+ let value;
1874
+ args.forEach((arg, position) => {
1875
+ if (arg === name) {
1876
+ value = args[position + 1];
1877
+ } else if (arg.startsWith(`${name}=`)) {
1878
+ value = arg.slice(name.length + 1);
1879
+ }
1880
+ });
1881
+ return value;
1882
+ };
1883
+ const fromConfig = (name, fallback) => typeof options[name] === "string" ? options[name] : fallback;
1884
+ return {
1885
+ incremental: options.incremental === true || args.includes("--incremental"),
1886
+ force: options.force === true || args.includes("--force"),
1887
+ incrementalFile: valueOf("--incrementalFile") ?? fromConfig("incrementalFile", "reports/stryker-incremental.json"),
1888
+ tempDirName: valueOf("--tempDirName") ?? fromConfig("tempDirName", ".stryker-tmp")
1889
+ };
1890
+ }
1891
+ function comparableArgs(args) {
1892
+ return args.filter((arg) => arg !== "--force");
1893
+ }
1894
+ function objectOf(value) {
1895
+ return typeof value === "object" && value !== null ? value : {};
1896
+ }
1897
+ function reportFiles(config) {
1898
+ const options = objectOf(config);
1899
+ const fileName = (reporter, fallback) => {
1900
+ const named = objectOf(options[reporter]).fileName;
1901
+ return typeof named === "string" ? named : fallback;
1902
+ };
1903
+ return [
1904
+ fileName("htmlReporter", "reports/mutation/mutation.html"),
1905
+ fileName("jsonReporter", "reports/mutation/mutation.json")
1906
+ ];
1907
+ }
1908
+ var WILDCARDS = /* @__PURE__ */ new Map([
1909
+ ["**/", "(?:.*/)?"],
1910
+ ["*", "[^/]*"],
1911
+ ["?", "[^/]"]
1912
+ ]);
1913
+ function globSource(glob) {
1914
+ return glob.split(/(\*\*\/|\*|\?)/).map((part) => WILDCARDS.get(part) ?? part.replace(/\W/g, "\\$&")).join("");
1915
+ }
1916
+ function strykerIgnores(config) {
1917
+ const patterns = [objectOf(config).ignorePatterns].flat().filter((pattern) => typeof pattern === "string");
1918
+ if (patterns.some((pattern) => pattern.startsWith("!"))) {
1919
+ return () => false;
1920
+ }
1921
+ const rules = patterns.map((pattern) => {
1922
+ const directoryOnly = pattern.endsWith("/");
1923
+ const body = pattern.replace(/\/+$/, "");
1924
+ const anchored = body.includes("/");
1925
+ const expression = new RegExp(`^${globSource(body.replace(/^\//, ""))}$`, "i");
1926
+ return { directoryOnly, anchored, expression };
1927
+ });
1928
+ return (relative) => {
1929
+ const parts = relative.split("/");
1930
+ return rules.some(
1931
+ (rule) => parts.some((part, index) => {
1932
+ if (rule.directoryOnly && index === parts.length - 1) {
1933
+ return false;
1934
+ }
1935
+ const target = rule.anchored ? parts.slice(0, index + 1).join("/") : part;
1936
+ return rule.expression.test(target);
1937
+ })
1938
+ );
1939
+ };
1940
+ }
1941
+
1942
+ // src/core/affected.ts
1943
+ var CONFIG_NAMES = [
1944
+ /^package\.json$/,
1945
+ /^(?:package-lock|npm-shrinkwrap)\.json$/,
1946
+ /^test-gates\.json$/,
1947
+ /^[jt]sconfig(?:\..*)?\.json$/,
1948
+ /\.config\.[^.]+$/,
1949
+ /\.config\.[^.]+\.[^.]+$/,
1950
+ /^(?:jest|vitest|vite|stryker|babel)[.-]/,
1951
+ /^\./,
1952
+ // Declarations need no import to take effect.
1953
+ /\.d\.[cm]?ts$/
1954
+ ];
1955
+ var ROOT_MOCKS = "__mocks__";
1956
+ var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc|html?)$/i;
1957
+ var ENTRY_PAGE = /^index\.html?$/i;
1958
+ var PASSIVE = /\.(?:[cm]?[jt]sx?|json|snap|css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico)$/i;
1959
+ function kindOf(key, subproject) {
1960
+ const segments = localSegments(key, subproject);
1961
+ const name = segments[segments.length - 1];
1962
+ if (segments[0] === ROOT_MOCKS || segments.includes("node_modules") || CONFIG_NAMES.some((pattern) => pattern.test(name))) {
1963
+ return "config";
1964
+ }
1965
+ if (isNestedMock(key, subproject)) {
1966
+ return "other";
1967
+ }
1968
+ if (DOCUMENT.test(name) && !ENTRY_PAGE.test(name)) {
1969
+ return "document";
1970
+ }
1971
+ const inside = subproject === "" || key.startsWith(`${subproject}/`);
1972
+ if (!inside || segments.length === 1 || !PASSIVE.test(name)) {
1973
+ return "other";
1974
+ }
1975
+ return "passive";
1976
+ }
1977
+ function localSegments(key, subproject) {
1978
+ return (key.startsWith(`${subproject}/`) ? key.slice(subproject.length + 1) : key).split("/");
1979
+ }
1980
+ function isNestedMock(key, subproject) {
1981
+ return localSegments(key, subproject).slice(1, -1).includes(ROOT_MOCKS);
1982
+ }
1983
+ function gateDependencies(input) {
1984
+ const closureOf = (key) => Object.hasOwn(input.closures, key) ? input.closures[key] : [key];
1985
+ const blind = input.blind ?? {};
1986
+ const result = /* @__PURE__ */ Object.create(null);
1987
+ for (const gate of input.gates) {
1988
+ const covering = Object.hasOwn(input.covering, gate.key) ? input.covering[gate.key] : [];
1989
+ const specs = /* @__PURE__ */ new Set([...gate.specs, ...covering]);
1990
+ for (const test of input.tests) {
1991
+ if (closureOf(test).includes(gate.key)) {
1992
+ specs.add(test);
1993
+ }
1994
+ }
1995
+ const files = new Set(closureOf(gate.key));
1996
+ for (const spec of specs) {
1997
+ closureOf(spec).forEach((file) => files.add(file));
1998
+ }
1999
+ const sorted2 = [...files].sort();
2000
+ const reason = sorted2.find((file) => Object.hasOwn(blind, file));
2001
+ result[gate.key] = {
2002
+ specs: [...specs].sort(),
2003
+ files: sorted2,
2004
+ ...reason === void 0 ? {} : { blind: blind[reason] }
2005
+ };
2006
+ }
2007
+ return result;
2008
+ }
2009
+ function planAffected(input) {
2010
+ const recorded = input.before.affected;
2011
+ if (recorded === void 0) {
2012
+ return {
2013
+ full: "the last run did not write down what each gate depends on (it ran in CI, with an older version, or could not tell)"
2014
+ };
2015
+ }
2016
+ const changed = differingKeys(input.before.files, input.now);
2017
+ const reached = new Set(input.gates.flatMap((gate) => gate.files));
2018
+ Object.values(recorded.gates).forEach((gate) => gate.files.forEach((file) => reached.add(file)));
2019
+ const config = new Set(input.config);
2020
+ const everywhere = changed.filter((key) => {
2021
+ const kind = kindOf(key, input.subproject);
2022
+ return config.has(key) || kind === "config" || kind === "other" && !reached.has(key) || // A mock that is gone may have been one of a package, which is used by name for every spec.
2023
+ isNestedMock(key, input.subproject) && !Object.hasOwn(input.now, key);
2024
+ });
2025
+ if (everywhere.length > 0) {
2026
+ return { full: `changed since the last run, and may affect every gate: ${listed(everywhere)}` };
2027
+ }
2028
+ const rerun = [];
2029
+ const reused = [];
2030
+ for (const gate of input.gates) {
2031
+ const last = Object.hasOwn(recorded.gates, gate.path) ? recorded.gates[gate.path] : void 0;
2032
+ if (last === void 0) {
2033
+ rerun.push({ gate: gate.path, changed: [] });
2034
+ continue;
2035
+ }
2036
+ const blind = gate.blind ?? last.blind;
2037
+ if (blind !== void 0) {
2038
+ if (changed.length > 0 || input.seeded === true) {
2039
+ rerun.push({ gate: gate.path, changed, blind });
2040
+ } else {
2041
+ reused.push(gate.path);
2042
+ }
2043
+ continue;
2044
+ }
2045
+ const files = [.../* @__PURE__ */ new Set([...last.files, ...gate.files])];
1613
2046
  const differing = files.filter((file) => input.before.files[file] !== input.now[file]).sort();
1614
2047
  if (differing.length > 0) {
1615
2048
  rerun.push({ gate: gate.path, changed: differing });
@@ -1624,23 +2057,33 @@ function withoutDots(file) {
1624
2057
  }
1625
2058
  function timeoutRanges(last, gates) {
1626
2059
  const ranges = [];
2060
+ const rerun = /* @__PURE__ */ new Set();
2061
+ const taken = /* @__PURE__ */ new Set();
1627
2062
  const files = last.files ?? {};
1628
2063
  for (const named of gates) {
1629
- const gate = withoutDots(named);
1630
- const mutants = Object.hasOwn(files, gate) ? files[gate]?.mutants : void 0;
1631
- if (mutants === void 0) {
2064
+ const gate = withoutDots(named.path);
2065
+ const result = Object.hasOwn(files, gate) ? files[gate] : void 0;
2066
+ if (result?.mutants === void 0) {
1632
2067
  continue;
1633
2068
  }
2069
+ const { mutants } = result;
2070
+ const expected = expectedTimeoutMutants(named, result.source, mutants);
1634
2071
  for (const mutant of mutants) {
1635
- if (mutant.status === "Timeout") {
1636
- const { start, end } = mutant.location;
1637
- ranges.push(`${gate}:${start.line}:${start.column - 1}-${end.line}:${end.column}`);
2072
+ if (mutant.status !== "Timeout") {
2073
+ continue;
2074
+ }
2075
+ if (expected.has(mutant)) {
2076
+ taken.add(mutant);
2077
+ continue;
1638
2078
  }
2079
+ rerun.add(mutant);
2080
+ const { start, end } = mutant.location;
2081
+ ranges.push(`${gate}:${start.line}:${start.column - 1}-${end.line}:${end.column}`);
1639
2082
  }
1640
2083
  }
1641
- return [...new Set(ranges)];
2084
+ return { ranges: [...new Set(ranges)], rerun: rerun.size, expected: taken.size };
1642
2085
  }
1643
- function describePlan(plan, shown = 5, timeouts = 0) {
2086
+ function describePlan(plan, shown = 5, timeouts = 0, expected = 0) {
1644
2087
  if ("full" in plan) {
1645
2088
  return [
1646
2089
  `test-gates mutation --affected: running every mutant again (--force), the results of the last run are not reused: ${plan.full}`
@@ -1650,19 +2093,35 @@ function describePlan(plan, shown = 5, timeouts = 0) {
1650
2093
  const lines = [
1651
2094
  `test-gates mutation --affected: ${plan.reused.length} of ${total} gate(s) reused, ${plan.rerun.length} run again`
1652
2095
  ];
1653
- for (const { gate, changed } of plan.rerun.slice(0, shown)) {
2096
+ const told = plan.rerun.filter((entry) => entry.blind === void 0);
2097
+ for (const { gate, changed } of told.slice(0, shown)) {
1654
2098
  lines.push(
1655
2099
  changed.length === 0 ? ` ${gate}: the last run left no result of it that can be reused` : ` ${gate}: changed ${listed(changed)}`
1656
2100
  );
1657
2101
  }
1658
- if (plan.rerun.length > shown) {
1659
- lines.push(` and ${plan.rerun.length - shown} more`);
2102
+ if (told.length > shown) {
2103
+ lines.push(` and ${told.length - shown} more`);
2104
+ }
2105
+ const reasons = [
2106
+ ...new Set(plan.rerun.flatMap((entry) => entry.blind === void 0 ? [] : [entry.blind]))
2107
+ ];
2108
+ if (reasons.length > 0) {
2109
+ const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
2110
+ lines.push(
2111
+ ` ${plan.rerun.length - told.length} gate(s) run again for any change, as what they depend on cannot be told: ${reasons[0]}${more}`
2112
+ );
1660
2113
  }
1661
2114
  if (timeouts > 0) {
1662
2115
  lines.push(` and ${timeouts} mutant(s) of reused gates that timed out in the last run`);
1663
2116
  }
2117
+ if (expected > 0) {
2118
+ lines.push(` ${describeExpected(expected)}`);
2119
+ }
1664
2120
  return lines;
1665
2121
  }
2122
+ function describeExpected(expected) {
2123
+ return `${expected} mutant(s) that timed out in the last run are taken over: expectedTimeouts lists them`;
2124
+ }
1666
2125
 
1667
2126
  // src/core/doctor.ts
1668
2127
  var NOT_THE_HELPER = "the runner config is not made with";
@@ -1697,12 +2156,22 @@ function diagnose(facts) {
1697
2156
  fix: "write such a path from the directory of its file (path.resolve(__dirname, \u2026)) so that it can be followed, or keep it out of the files the run loads"
1698
2157
  });
1699
2158
  }
2159
+ const everyBlind = facts.blindGates.length >= facts.gates;
1700
2160
  if (facts.blind !== null) {
1701
2161
  findings.push({
1702
2162
  level: facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
1703
2163
  text: `with --affected, a change of any file runs every mutant: what a gate depends on cannot be told (${facts.blind})`,
1704
2164
  fix: "keep what reads files or loads modules by a computed name out of what the gates and their specs import, or run without --affected"
1705
2165
  });
2166
+ } else if (facts.blindGates.length > 0) {
2167
+ const gates = facts.blindGates.map((one) => one.gate);
2168
+ const reasons = [...new Set(facts.blindGates.map((one) => one.reason))];
2169
+ const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
2170
+ findings.push({
2171
+ level: everyBlind && facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
2172
+ text: `with --affected, a change of any file runs ${gates.length} of ${facts.gates} gate(s) again (${listed(gates)}): what they depend on cannot be told (${reasons[0]}${more})`,
2173
+ fix: "keep what reads files by a path no import names out of what those gates and their specs import"
2174
+ });
1706
2175
  }
1707
2176
  if (facts.scope !== null && "every" in facts.scope) {
1708
2177
  findings.push({
@@ -1711,6 +2180,21 @@ function diagnose(facts) {
1711
2180
  fix: facts.scope.every.startsWith(NOT_THE_HELPER) ? "make the runner config with createJestGatesConfig or createVitestGatesConfig" : null
1712
2181
  });
1713
2182
  }
2183
+ if (facts.sandboxes !== null) {
2184
+ const { tempDir, count, kept, running } = facts.sandboxes;
2185
+ findings.push(
2186
+ running === null ? {
2187
+ level: "note",
2188
+ text: `${count} sandbox(es) an interrupted Stryker run left in ${tempDir}: a tool that walks the subproject (a test runner, a script) finds a copy of every file in them`,
2189
+ fix: kept === null ? "the next test-gates mutation removes them" : `remove them by hand; the next test-gates mutation keeps them, because ${kept}`
2190
+ } : {
2191
+ // The sandboxes may be those of that run: removing them would break it.
2192
+ level: "note",
2193
+ text: `another test-gates mutation is running in this subproject (pid ${running}): the ${count} sandbox(es) in ${tempDir} are in use`,
2194
+ fix: "leave them; the first test-gates mutation after that run ends removes what is left"
2195
+ }
2196
+ );
2197
+ }
1714
2198
  if (facts.hooks.length === 0) {
1715
2199
  findings.push({
1716
2200
  level: "note",
@@ -1718,7 +2202,8 @@ function diagnose(facts) {
1718
2202
  fix: null
1719
2203
  });
1720
2204
  }
1721
- for (const hook of facts.hooks.filter((one) => !one.affected && facts.blind === null)) {
2205
+ const toldApart = facts.blind === null && !everyBlind;
2206
+ for (const hook of facts.hooks.filter((one) => !one.affected && toldApart)) {
1722
2207
  findings.push({
1723
2208
  level: "problem",
1724
2209
  text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
@@ -1744,86 +2229,399 @@ function formatFindings(findings) {
1744
2229
  var RUNS_MUTATION = /(?<![\w-])test-gates(?:\.js)?\s+mutation(?![\w-])|(?<![\w:-])test:gates:mutation(?![\w:-])/;
1745
2230
  var TARGET = /(?:--dir|--prefix|--workspace|-w)(?:=|\s+)(['"]?)([^\s'";&|)]+)\1/;
1746
2231
  var CD = /\bcd\s+(['"]?)([^\s'";&|)]+)\1/;
2232
+ var NONE = [];
2233
+ var COMMAND = String.raw`(?<=(?:^|[\n;&|(){]|\b(?:then|else|do))\s*)`;
2234
+ var TOKEN = new RegExp(
2235
+ String.raw`\n|;;&?|;&|${COMMAND}(?:case\s+(\S+)\s+in|for\s+(\w+)(?:\s+in\s+([^;\n]*))?|(esac|do|done))(?![\w-])`,
2236
+ "g"
2237
+ );
2238
+ var PATTERN = /\s*(?:esac(?![\w-])|\(?([^()\n;]*)\))/y;
2239
+ var VARIABLE = /^("?)\$(?:(\w+)|\{(\w+)\})\1$/;
2240
+ var REFERENCE = /\$(?:(\w+)|\{(\w+)\})/g;
2241
+ function namesOf(words) {
2242
+ const names = words.map((word) => word.trim().replace(/^(["'])(.*)\1$/, "$2"));
2243
+ return names.some((name) => name === "" || /[$*?[{}~\\`"']/.test(name)) ? null : names;
2244
+ }
2245
+ function partsOf(text) {
2246
+ const parts = [];
2247
+ let scopes = NONE;
2248
+ let start = 0;
2249
+ let loop = null;
2250
+ const enter = (scope) => {
2251
+ scopes = [...scopes, scope];
2252
+ };
2253
+ const leave = (kind) => {
2254
+ const left = scopes.at(-1)?.kind === kind;
2255
+ scopes = scopes.slice(0, -1);
2256
+ return left;
2257
+ };
2258
+ TOKEN.lastIndex = 0;
2259
+ for (; ; ) {
2260
+ const token = TOKEN.exec(text);
2261
+ parts.push({ text: text.slice(start, token?.index), scopes });
2262
+ if (token === null) {
2263
+ return scopes.length === 0 ? parts : null;
2264
+ }
2265
+ const [word, subject, variable, list, keyword] = token;
2266
+ let awaiting = false;
2267
+ if (subject !== void 0) {
2268
+ const named = VARIABLE.exec(subject);
2269
+ enter({
2270
+ kind: "case",
2271
+ id: /* @__PURE__ */ Symbol(),
2272
+ variable: named?.[2] ?? named?.[3] ?? null,
2273
+ names: null
2274
+ });
2275
+ awaiting = true;
2276
+ } else if (variable !== void 0) {
2277
+ loop = {
2278
+ kind: "loop",
2279
+ id: /* @__PURE__ */ Symbol(),
2280
+ variable,
2281
+ names: list === void 0 ? null : namesOf(list.trim().split(/\s+/))
2282
+ };
2283
+ } else if (keyword === "do") {
2284
+ enter(loop ?? { kind: "loop", id: /* @__PURE__ */ Symbol(), variable: null, names: null });
2285
+ loop = null;
2286
+ } else if (keyword === "done" || keyword === "esac") {
2287
+ if (!leave(keyword === "done" ? "loop" : "case")) {
2288
+ return null;
2289
+ }
2290
+ } else if (word === ";&") {
2291
+ return null;
2292
+ } else if (word !== "\n") {
2293
+ if (scopes.at(-1)?.kind !== "case") {
2294
+ return null;
2295
+ }
2296
+ awaiting = true;
2297
+ }
2298
+ start = TOKEN.lastIndex;
2299
+ if (awaiting) {
2300
+ PATTERN.lastIndex = start;
2301
+ const pattern = PATTERN.exec(text);
2302
+ if (pattern === null) {
2303
+ return null;
2304
+ }
2305
+ start = PATTERN.lastIndex;
2306
+ TOKEN.lastIndex = start;
2307
+ const top = scopes.at(-1);
2308
+ scopes = scopes.slice(0, -1);
2309
+ if (pattern[1] !== void 0) {
2310
+ enter({ ...top, names: namesOf(pattern[1].split("|")) });
2311
+ }
2312
+ }
2313
+ }
2314
+ }
2315
+ function sibling(of, scopes) {
2316
+ const apart = of.findIndex((scope, at) => scope !== scopes[at]);
2317
+ return apart !== -1 && of[apart].id === scopes[apart]?.id;
2318
+ }
2319
+ function expansions(target, scopes) {
2320
+ return [...target.matchAll(REFERENCE)].reduce(
2321
+ (words, [reference, plain2, braced]) => {
2322
+ const names = [...scopes].reverse().find((scope) => scope.variable === (plain2 ?? braced))?.names ?? [reference];
2323
+ return words.flatMap((word) => names.map((name) => word.replace(reference, name)));
2324
+ },
2325
+ [target]
2326
+ );
2327
+ }
1747
2328
  function hookUses(input) {
1748
2329
  const lines = input.text.split("\n").map((line) => line.replace(/(?:^|\s)#.*/, ""));
2330
+ const parts = partsOf(lines.join("\n")) ?? lines.map((text) => ({ text, scopes: NONE }));
1749
2331
  const uses = [];
1750
- lines.forEach((line, index) => {
1751
- if (!RUNS_MUTATION.test(line)) {
2332
+ const cds = parts.map((part) => ({ cd: CD.exec(part.text)?.[2], scopes: part.scopes }));
2333
+ parts.forEach((part, index) => {
2334
+ if (!RUNS_MUTATION.test(part.text)) {
1752
2335
  return;
1753
2336
  }
1754
- const named = TARGET.exec(line) ?? CD.exec(line);
1755
- const target = named?.[2] ?? lines.slice(0, index).reverse().map((before) => CD.exec(before)?.[2]).find((found) => found !== void 0);
1756
- const normalized = target?.split("/").filter((part) => part !== "." && part !== "").join("/");
1757
- const ours = normalized === void 0 || normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
2337
+ const named = TARGET.exec(part.text) ?? CD.exec(part.text);
2338
+ const before = cds.slice(0, index).reverse().find((one) => one.cd !== void 0 && !sibling(one.scopes, part.scopes));
2339
+ const target = named?.[2] ?? before?.cd;
2340
+ const scopes = named !== null || before?.scopes === part.scopes ? part.scopes : NONE;
2341
+ const ours = target === void 0 || expansions(target, scopes).some((word) => {
2342
+ const normalized = word.split("/").filter((one) => one !== "." && one !== "").join("/");
2343
+ return normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
2344
+ });
1758
2345
  if (ours) {
1759
2346
  uses.push({
1760
2347
  file: input.file,
1761
- affected: /--affected\b/.test(line) || /test:gates:mutation\b/.test(line) && input.script.includes("--affected")
2348
+ affected: /--affected\b/.test(part.text) || /test:gates:mutation\b/.test(part.text) && input.script.includes("--affected")
1762
2349
  });
1763
2350
  }
1764
2351
  });
1765
2352
  return uses;
1766
2353
  }
1767
2354
 
1768
- // src/snapshot.ts
2355
+ // src/sandboxes.ts
1769
2356
  import { spawnSync } from "child_process";
2357
+ import fs4 from "fs";
2358
+ import path4 from "path";
2359
+
2360
+ // src/core/sandboxes.ts
2361
+ var SANDBOX = /^sandbox-[A-Za-z0-9]{6}$/;
2362
+ var MARKER = /^test-gates-run-([1-9][0-9]*)$/;
2363
+ function markerName(pid) {
2364
+ return `test-gates-run-${pid}`;
2365
+ }
2366
+ function markerText(pid, started) {
2367
+ return `${pid}
2368
+ ${started ?? ""}
2369
+ `;
2370
+ }
2371
+ function markerAlive(input) {
2372
+ const recorded = input.text?.split("\n")[1]?.trim() || null;
2373
+ return input.there && (recorded === null || input.started === null || sameStart(recorded, input.started));
2374
+ }
2375
+ var START_SLACK_SECONDS = 5;
2376
+ function sameStart(a, b) {
2377
+ const difference = Math.abs(Date.parse(a) - Date.parse(b));
2378
+ return Number.isNaN(difference) ? a === b : difference <= START_SLACK_SECONDS * 1e3;
2379
+ }
2380
+ function insideSubproject(tempDir) {
2381
+ const parts = tempDir.split("/");
2382
+ return !/^[A-Za-z]:/.test(tempDir) && parts.every((part) => part !== "" && part !== "." && part !== "..");
2383
+ }
2384
+ function planSandboxes(facts) {
2385
+ const sandboxes = facts.entries.filter((entry) => entry.directory && SANDBOX.test(entry.name)).map((entry) => entry.name).sort();
2386
+ if (facts.tempDir === null) {
2387
+ return {
2388
+ kept: sandboxes,
2389
+ why: "the Stryker config could not be loaded to see where they are",
2390
+ running: null
2391
+ };
2392
+ }
2393
+ if (!insideSubproject(facts.tempDir)) {
2394
+ return {
2395
+ kept: sandboxes,
2396
+ why: `tempDirName (${facts.tempDir}) is not a directory inside the subproject`,
2397
+ running: null
2398
+ };
2399
+ }
2400
+ if (facts.keeps) {
2401
+ return { kept: sandboxes, why: "the Stryker config sets cleanTempDir: false", running: null };
2402
+ }
2403
+ const markers = [];
2404
+ for (const entry of facts.entries) {
2405
+ const pid = MARKER.exec(entry.name)?.[1];
2406
+ if (pid === void 0 || entry.directory) {
2407
+ continue;
2408
+ }
2409
+ if (facts.alive(Number(pid), entry.name)) {
2410
+ return {
2411
+ kept: sandboxes,
2412
+ why: `another test-gates mutation is running in this subproject (pid ${pid})`,
2413
+ running: Number(pid)
2414
+ };
2415
+ }
2416
+ markers.push(entry.name);
2417
+ }
2418
+ return { remove: sandboxes, markers };
2419
+ }
2420
+
2421
+ // src/sandboxes.ts
2422
+ function there(pid) {
2423
+ try {
2424
+ process.kill(pid, 0);
2425
+ return true;
2426
+ } catch (error) {
2427
+ return error.code === "EPERM";
2428
+ }
2429
+ }
2430
+ function startedAt(pid) {
2431
+ const result = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
2432
+ encoding: "utf8",
2433
+ env: { ...process.env, LC_ALL: "C" },
2434
+ timeout: 5e3
2435
+ });
2436
+ const started = result.status === 0 ? result.stdout.trim() : "";
2437
+ return started === "" ? null : started;
2438
+ }
2439
+ function alive(dir) {
2440
+ return (pid, marker) => {
2441
+ if (!there(pid)) {
2442
+ return false;
2443
+ }
2444
+ let text = null;
2445
+ try {
2446
+ text = fs4.readFileSync(path4.join(dir, marker), "utf8");
2447
+ } catch {
2448
+ }
2449
+ return markerAlive({ there: true, text, started: startedAt(pid) });
2450
+ };
2451
+ }
2452
+ function relativeTempDir(subproject, dir) {
2453
+ let real = dir;
2454
+ try {
2455
+ real = fs4.realpathSync(dir);
2456
+ } catch {
2457
+ }
2458
+ return path4.relative(fs4.realpathSync(subproject), real).split(path4.sep).join("/");
2459
+ }
2460
+ function sandboxPlan(subproject, setting) {
2461
+ const tempDir = setting.dir === null ? null : relativeTempDir(subproject, setting.dir);
2462
+ let entries = [];
2463
+ if (setting.dir !== null) {
2464
+ try {
2465
+ entries = fs4.readdirSync(setting.dir, { withFileTypes: true });
2466
+ } catch {
2467
+ }
2468
+ }
2469
+ const plan = planSandboxes({
2470
+ tempDir,
2471
+ entries: entries.map((entry) => ({ name: entry.name, directory: entry.isDirectory() })),
2472
+ keeps: setting.keeps,
2473
+ alive: alive(setting.dir ?? subproject)
2474
+ });
2475
+ return { plan, shown: tempDir ?? ".stryker-tmp" };
2476
+ }
2477
+ function removeStaleSandboxes(subproject, setting) {
2478
+ const { plan, shown } = sandboxPlan(subproject, setting);
2479
+ if ("kept" in plan) {
2480
+ if (plan.kept.length === 0) {
2481
+ return [];
2482
+ }
2483
+ return plan.running === null ? [
2484
+ `test-gates mutation: ${plan.kept.length} sandbox(es) left in ${shown} by an earlier Stryker run are kept: ${plan.why}`
2485
+ ] : [
2486
+ `test-gates mutation: ${plan.why}, so the ${plan.kept.length} sandbox(es) in ${shown} are kept: they are in use`
2487
+ ];
2488
+ }
2489
+ const dir = setting.dir;
2490
+ for (const marker of plan.markers) {
2491
+ try {
2492
+ fs4.rmSync(path4.join(dir, marker), { force: true });
2493
+ } catch {
2494
+ }
2495
+ }
2496
+ const failed = [];
2497
+ for (const sandbox of plan.remove) {
2498
+ try {
2499
+ fs4.rmSync(path4.join(dir, sandbox), { recursive: true, force: true });
2500
+ } catch (error) {
2501
+ failed.push(`${sandbox} (${error.message})`);
2502
+ }
2503
+ }
2504
+ const removed = plan.remove.length - failed.length;
2505
+ return [
2506
+ ...removed > 0 ? [
2507
+ `test-gates mutation: removed ${removed} sandbox(es) an earlier Stryker run left in ${shown}`
2508
+ ] : [],
2509
+ ...failed.length > 0 ? [`test-gates mutation: could not remove from ${shown}: ${failed.join(", ")}`] : []
2510
+ ];
2511
+ }
2512
+ function markRun(subproject, setting) {
2513
+ const dir = setting.dir;
2514
+ if (dir === null || !insideSubproject(relativeTempDir(subproject, dir))) {
2515
+ return () => void 0;
2516
+ }
2517
+ const marker = path4.join(dir, markerName(process.pid));
2518
+ try {
2519
+ fs4.mkdirSync(dir, { recursive: true });
2520
+ if (!insideSubproject(relativeTempDir(subproject, dir))) {
2521
+ return () => void 0;
2522
+ }
2523
+ fs4.writeFileSync(marker, markerText(process.pid, startedAt(process.pid)));
2524
+ } catch {
2525
+ return () => void 0;
2526
+ }
2527
+ return () => {
2528
+ try {
2529
+ fs4.rmSync(marker, { force: true });
2530
+ fs4.rmdirSync(dir);
2531
+ } catch {
2532
+ }
2533
+ };
2534
+ }
2535
+
2536
+ // src/snapshot.ts
2537
+ import { spawnSync as spawnSync2 } from "child_process";
1770
2538
  import { createHash } from "crypto";
1771
- import fs5 from "fs";
2539
+ import fs6 from "fs";
1772
2540
  import { createRequire } from "module";
1773
2541
  import os from "os";
1774
- import path5 from "path";
2542
+ import path6 from "path";
1775
2543
 
1776
2544
  // src/commands/loaded-files.ts
1777
- import fs4 from "fs";
2545
+ import fs5 from "fs";
1778
2546
  import { builtinModules } from "module";
1779
- import path4 from "path";
2547
+ import path5 from "path";
1780
2548
  var SCRIPT = /\.[cm]?[jt]sx?$/;
1781
2549
  var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
1782
2550
  var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1783
2551
  var QUOTED = /(['"`])([^'"`\n]+)\1/g;
1784
- var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule)\(\s*(['"`])([^'"`\n]+)\1/g;
2552
+ var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|createMockFromModule|genMockFromModule)\s*(?:<[^()\n]*>)?\s*\(\s*(['"`])([^'"`\n]+)\1/g;
1785
2553
  function isFile(file) {
1786
2554
  try {
1787
- return fs4.statSync(file).isFile();
2555
+ return fs5.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
1788
2556
  } catch {
1789
2557
  return false;
1790
2558
  }
1791
2559
  }
1792
- function resolveFile(base, directory = true) {
1793
- const candidates = [
1794
- base,
1795
- ...EXTENSIONS.map((extension) => base + extension),
2560
+ var resolved = null;
2561
+ function resolving(read2) {
2562
+ if (resolved !== null) {
2563
+ return read2();
2564
+ }
2565
+ resolved = /* @__PURE__ */ new Map();
2566
+ try {
2567
+ return read2();
2568
+ } finally {
2569
+ resolved = null;
2570
+ }
2571
+ }
2572
+ var DECLARATIONS = [".d.ts", ".d.mts", ".d.cts"];
2573
+ function resolveFiles(base, directory = true, declarations = true) {
2574
+ const key = `${Number(directory)}${Number(declarations)}${base}`;
2575
+ const known = resolved?.get(key);
2576
+ if (known !== void 0) {
2577
+ return [...known];
2578
+ }
2579
+ const found = filesOf(base, directory, declarations);
2580
+ resolved?.set(key, found);
2581
+ return [...found];
2582
+ }
2583
+ function filesOf(base, directory, declarations) {
2584
+ const candidates = (extensions) => [
2585
+ ...extensions.map((extension) => base + extension),
1796
2586
  // `./fee.js` written in a TypeScript file is `./fee.ts`
1797
- ...EXTENSIONS.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
1798
- ...directory ? EXTENSIONS.map((extension) => path4.join(base, `index${extension}`)) : []
2587
+ ...extensions.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
2588
+ ...directory ? extensions.map((extension) => path5.join(base, `index${extension}`)) : []
1799
2589
  ];
1800
- return candidates.find(isFile) ?? null;
2590
+ const data = [`${base}.json`, ...directory ? [path5.join(base, "index.json")] : []];
2591
+ const scripts = [.../* @__PURE__ */ new Set([base, ...candidates(EXTENSIONS), ...data])].filter(isFile);
2592
+ return scripts.length > 0 || !declarations ? scripts : [...new Set(candidates(DECLARATIONS))].filter(isFile);
2593
+ }
2594
+ function resolveFile(base, directory = true, declarations = true) {
2595
+ return resolveFiles(base, directory, declarations)[0] ?? null;
1801
2596
  }
1802
2597
  function installed(specifier, from) {
1803
2598
  const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
1804
- for (let current = path4.dirname(from); ; current = path4.dirname(current)) {
1805
- if (fs4.existsSync(path4.join(current, "node_modules", name))) {
2599
+ for (let current = path5.dirname(from); ; current = path5.dirname(current)) {
2600
+ if (fs5.existsSync(path5.join(current, "node_modules", name))) {
1806
2601
  return true;
1807
2602
  }
1808
- if (current === path4.dirname(current)) {
2603
+ if (current === path5.dirname(current)) {
1809
2604
  return false;
1810
2605
  }
1811
2606
  }
1812
2607
  }
1813
2608
  function loadedFiles(manifest, configFile) {
2609
+ return resolving(() => loadedFilesNow(manifest, configFile));
2610
+ }
2611
+ function loadedFilesNow(manifest, configFile) {
1814
2612
  const aliases = manifest.settings.importAliases;
1815
- const relative = (file) => path4.relative(manifest.dir, file).split(path4.sep).join("/");
2613
+ const relative = (file) => path5.relative(manifest.dir, file).split(path5.sep).join("/");
1816
2614
  const target = (specifier, from) => {
1817
2615
  if (specifier.startsWith(".")) {
1818
- return resolveFile(path4.resolve(path4.dirname(from), specifier));
2616
+ return resolveFiles(path5.resolve(path5.dirname(from), specifier));
1819
2617
  }
1820
2618
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
1821
- return alias === void 0 ? null : resolveFile(
1822
- path4.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
2619
+ return alias === void 0 ? [] : resolveFiles(
2620
+ path5.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
1823
2621
  );
1824
2622
  };
1825
2623
  const read2 = (file) => {
1826
- const source = fs4.readFileSync(file, "utf8");
2624
+ const source = fs5.readFileSync(file, "utf8");
1827
2625
  const imports = parseImports(source).filter((ref) => !ref.typeOnly);
1828
2626
  const specifiers = [
1829
2627
  .../* @__PURE__ */ new Set([
@@ -1835,7 +2633,7 @@ function loadedFiles(manifest, configFile) {
1835
2633
  return { source, specifiers, strings };
1836
2634
  };
1837
2635
  const named = /* @__PURE__ */ new Set();
1838
- const configs = configFile === null ? [] : [path4.resolve(manifest.dir, configFile)];
2636
+ const configs = configFile === null ? [] : [path5.resolve(manifest.dir, configFile)];
1839
2637
  const seenConfigs = new Set(configs);
1840
2638
  const entry = configs[0];
1841
2639
  for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
@@ -1843,16 +2641,18 @@ function loadedFiles(manifest, configFile) {
1843
2641
  const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
1844
2642
  for (const text of paths) {
1845
2643
  const stripped = text.replace(/^<rootDir>\//, "");
1846
- const found = resolveFile(path4.resolve(path4.dirname(file), stripped), false) ?? resolveFile(path4.resolve(manifest.dir, stripped), false);
1847
- if (found !== null && SCRIPT.test(found)) {
1848
- named.add(found);
2644
+ const here = resolveFiles(path5.resolve(path5.dirname(file), stripped), false);
2645
+ const found = here.length > 0 ? here : resolveFiles(path5.resolve(manifest.dir, stripped), false);
2646
+ for (const one of found.filter((candidate) => SCRIPT.test(candidate))) {
2647
+ named.add(one);
1849
2648
  }
1850
2649
  }
1851
2650
  for (const specifier of specifiers.filter((one) => isProjectImport(one, aliases))) {
1852
- const found = target(specifier, file);
1853
- if (found !== null && SCRIPT.test(found) && !seenConfigs.has(found)) {
1854
- seenConfigs.add(found);
1855
- configs.push(found);
2651
+ for (const found of target(specifier, file)) {
2652
+ if (SCRIPT.test(found) && !seenConfigs.has(found)) {
2653
+ seenConfigs.add(found);
2654
+ configs.push(found);
2655
+ }
1856
2656
  }
1857
2657
  }
1858
2658
  }
@@ -1869,10 +2669,10 @@ function loadedFiles(manifest, configFile) {
1869
2669
  for (const specifier of specifiers) {
1870
2670
  if (isProjectImport(specifier, aliases)) {
1871
2671
  const found = target(specifier, file);
1872
- if (found === null) {
2672
+ if (found.length === 0) {
1873
2673
  lost ??= `${specifier} (imported by ${relative(file)})`;
1874
2674
  } else {
1875
- next.push(found);
2675
+ next.push(...found);
1876
2676
  }
1877
2677
  } else if (specifier.startsWith("node:") || builtinModules.includes(specifier)) {
1878
2678
  imports.push({ specifier, builtin: true });
@@ -1883,10 +2683,7 @@ function loadedFiles(manifest, configFile) {
1883
2683
  }
1884
2684
  }
1885
2685
  for (const text of strings.filter((one) => one.startsWith("."))) {
1886
- const found = resolveFile(path4.resolve(path4.dirname(file), text));
1887
- if (found !== null) {
1888
- next.push(found);
1889
- }
2686
+ next.push(...resolveFiles(path5.resolve(path5.dirname(file), text)));
1890
2687
  }
1891
2688
  const visited = {
1892
2689
  file: { path: relative(file), source, imports },
@@ -1916,7 +2713,7 @@ function loadedFiles(manifest, configFile) {
1916
2713
  const specs = [...new Set(manifest.gates.flatMap((gate) => gate.existingSpecs))];
1917
2714
  const roots = [
1918
2715
  closure(null, [...named]),
1919
- ...specs.map((spec) => closure(spec, [path4.join(manifest.dir, spec)]))
2716
+ ...specs.map((spec) => closure(spec, [path5.join(manifest.dir, spec)]))
1920
2717
  ];
1921
2718
  return { files: [...known.values()].map((visited) => visited.file), roots };
1922
2719
  }
@@ -1957,27 +2754,28 @@ var NEVER = ["node_modules", ".git"];
1957
2754
  var WIDENED_SHOWN = 5;
1958
2755
  var SCAN_LIMIT = 8 * 1024 * 1024;
1959
2756
  var UPWARD = /(['"`])((?:[^'"`\n]*\/)?\.\.(?:\/[^'"`\n]*)?)\1/g;
1960
- var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc"];
2757
+ var DOCUMENTS = [".md", ".markdown", ".txt", ".rst", ".adoc", ".html", ".htm"];
1961
2758
  var HERE = /^\$\{(?:__dirname|import\.meta\.dirname)\}(?=\/)/;
1962
2759
  var FROM_HERE = /\b(?:__dirname|import\.meta\.dirname)((?:\s*,\s*(['"])[^'"`\n]*\2)+)(\s*\))?/g;
1963
2760
  var LITERAL = /(['"])([^'"`\n]*)\1/g;
2761
+ var DOWNWARD = /(['"`])(\.\/[^'"`\n]*)\1/g;
1964
2762
  var isUpward = (segment) => segment.split("/").includes("..");
1965
2763
  function hashOf(content) {
1966
2764
  return createHash("sha256").update(content).digest("hex");
1967
2765
  }
1968
2766
  function hashFile(file) {
1969
2767
  try {
1970
- return hashOf(fs5.readFileSync(file));
2768
+ return hashOf(fs6.readFileSync(file));
1971
2769
  } catch {
1972
2770
  return null;
1973
2771
  }
1974
2772
  }
1975
2773
  function isInside(parent, child) {
1976
- const relative = path5.relative(parent, child);
1977
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
2774
+ const relative = path6.relative(parent, child);
2775
+ return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
1978
2776
  }
1979
2777
  function toPosix(file) {
1980
- return file.split(path5.sep).join("/");
2778
+ return file.split(path6.sep).join("/");
1981
2779
  }
1982
2780
  function gitEnvironment(env) {
1983
2781
  if (env.GIT_DIR === void 0 || env.GIT_WORK_TREE !== void 0) {
@@ -1988,7 +2786,7 @@ function gitEnvironment(env) {
1988
2786
  return rest;
1989
2787
  }
1990
2788
  function git(cwd, args) {
1991
- const result = spawnSync("git", args, {
2789
+ const result = spawnSync2("git", args, {
1992
2790
  cwd,
1993
2791
  env: gitEnvironment(process.env),
1994
2792
  encoding: "utf8",
@@ -1996,9 +2794,12 @@ function git(cwd, args) {
1996
2794
  });
1997
2795
  return result.error || result.status !== 0 ? null : result.stdout;
1998
2796
  }
2797
+ function globLiteral(relative) {
2798
+ return relative.replace(/[*?[\]\\]/g, "\\$&");
2799
+ }
1999
2800
  function gitLister(root) {
2000
2801
  return (directory, recursive) => {
2001
- const relative = toPosix(path5.relative(root, directory));
2802
+ const relative = globLiteral(toPosix(path6.relative(root, directory)));
2002
2803
  const pattern = recursive ? `${relative === "" ? "" : `${relative}/`}**` : `${relative === "" ? "" : `${relative}/`}*`;
2003
2804
  const listed2 = git(root, [
2004
2805
  "ls-files",
@@ -2017,8 +2818,8 @@ function gitLister(root) {
2017
2818
  }
2018
2819
  function walkLister(root) {
2019
2820
  const walk = (directory, recursive, found) => {
2020
- for (const entry of fs5.readdirSync(directory, { withFileTypes: true })) {
2021
- const file = path5.join(directory, entry.name);
2821
+ for (const entry of fs6.readdirSync(directory, { withFileTypes: true })) {
2822
+ const file = path6.join(directory, entry.name);
2022
2823
  if (NEVER.includes(entry.name)) {
2023
2824
  continue;
2024
2825
  }
@@ -2027,7 +2828,7 @@ function walkLister(root) {
2027
2828
  walk(file, true, found);
2028
2829
  }
2029
2830
  } else {
2030
- found.push(toPosix(path5.relative(root, file)));
2831
+ found.push(toPosix(path6.relative(root, file)));
2031
2832
  }
2032
2833
  }
2033
2834
  return found;
@@ -2037,16 +2838,16 @@ function walkLister(root) {
2037
2838
  function upTo(from, to) {
2038
2839
  const directories = [from];
2039
2840
  while (directories[directories.length - 1] !== to) {
2040
- directories.push(path5.dirname(directories[directories.length - 1]));
2841
+ directories.push(path6.dirname(directories[directories.length - 1]));
2041
2842
  }
2042
2843
  return directories;
2043
2844
  }
2044
2845
  function declaresWorkspaces(directory) {
2045
- if (fs5.existsSync(path5.join(directory, "pnpm-workspace.yaml"))) {
2846
+ if (fs6.existsSync(path6.join(directory, "pnpm-workspace.yaml"))) {
2046
2847
  return true;
2047
2848
  }
2048
2849
  try {
2049
- const manifest = JSON.parse(fs5.readFileSync(path5.join(directory, "package.json"), "utf8"));
2850
+ const manifest = JSON.parse(fs6.readFileSync(path6.join(directory, "package.json"), "utf8"));
2050
2851
  return manifest.workspaces !== void 0;
2051
2852
  } catch {
2052
2853
  return false;
@@ -2054,7 +2855,7 @@ function declaresWorkspaces(directory) {
2054
2855
  }
2055
2856
  function walkRoot(dir) {
2056
2857
  const candidates = withinBounds(dir);
2057
- const repository = candidates.find((directory) => fs5.existsSync(path5.join(directory, ".git")));
2858
+ const repository = candidates.find((directory) => fs6.existsSync(path6.join(directory, ".git")));
2058
2859
  if (repository !== void 0) {
2059
2860
  return repository;
2060
2861
  }
@@ -2063,11 +2864,11 @@ function walkRoot(dir) {
2063
2864
  function withinBounds(dir) {
2064
2865
  let home = null;
2065
2866
  try {
2066
- home = fs5.realpathSync(os.homedir());
2867
+ home = fs6.realpathSync(os.homedir());
2067
2868
  } catch {
2068
2869
  }
2069
- return upTo(dir, path5.parse(dir).root).filter(
2070
- (directory) => directory !== path5.parse(dir).root && (home === null || !isInside(directory, home))
2870
+ return upTo(dir, path6.parse(dir).root).filter(
2871
+ (directory) => directory !== path6.parse(dir).root && (home === null || !isInside(directory, home))
2071
2872
  );
2072
2873
  }
2073
2874
  function linkedPackages(nodeModules, root) {
@@ -2075,20 +2876,20 @@ function linkedPackages(nodeModules, root) {
2075
2876
  const visit = (directory, depth) => {
2076
2877
  let entries;
2077
2878
  try {
2078
- entries = fs5.readdirSync(directory, { withFileTypes: true });
2879
+ entries = fs6.readdirSync(directory, { withFileTypes: true });
2079
2880
  } catch {
2080
2881
  return;
2081
2882
  }
2082
2883
  for (const entry of entries) {
2083
- const file = path5.join(directory, entry.name);
2884
+ const file = path6.join(directory, entry.name);
2084
2885
  if (entry.isSymbolicLink()) {
2085
2886
  let target;
2086
2887
  try {
2087
- target = fs5.realpathSync(file);
2888
+ target = fs6.realpathSync(file);
2088
2889
  } catch {
2089
2890
  continue;
2090
2891
  }
2091
- if (target.split(path5.sep).includes("node_modules") || !fs5.statSync(target).isDirectory()) {
2892
+ if (target.split(path6.sep).includes("node_modules") || !fs6.statSync(target).isDirectory()) {
2092
2893
  continue;
2093
2894
  }
2094
2895
  if (!isInside(root, target)) {
@@ -2110,11 +2911,11 @@ function installedVersion(start, name) {
2110
2911
  for (; ; ) {
2111
2912
  try {
2112
2913
  const manifest = JSON.parse(
2113
- fs5.readFileSync(path5.join(current, "node_modules", name, "package.json"), "utf8")
2914
+ fs6.readFileSync(path6.join(current, "node_modules", name, "package.json"), "utf8")
2114
2915
  );
2115
2916
  return typeof manifest.version === "string" ? manifest.version : "unknown";
2116
2917
  } catch {
2117
- const parent = path5.dirname(current);
2918
+ const parent = path6.dirname(current);
2118
2919
  if (parent === current) {
2119
2920
  return null;
2120
2921
  }
@@ -2122,17 +2923,30 @@ function installedVersion(start, name) {
2122
2923
  }
2123
2924
  }
2124
2925
  }
2926
+ function nodeOnPath(cwd) {
2927
+ const result = spawnSync2("node", ["--version"], {
2928
+ cwd,
2929
+ encoding: "utf8",
2930
+ shell: process.platform === "win32",
2931
+ timeout: 3e4
2932
+ });
2933
+ const version = result.status === 0 ? result.stdout.trim() : "";
2934
+ return version === "" ? {} : { "Node.js on PATH": version };
2935
+ }
2125
2936
  function ownVersion() {
2126
2937
  const { version } = createRequire(import.meta.url)("../package.json");
2127
2938
  return version;
2128
2939
  }
2129
2940
  function snapshotOf(input) {
2130
- const dir = fs5.realpathSync(input.dir);
2941
+ return resolving(() => snapshotNow(input));
2942
+ }
2943
+ function snapshotNow(input) {
2944
+ const dir = fs6.realpathSync(input.dir);
2131
2945
  const top = git(dir, ["rev-parse", "--show-toplevel"]);
2132
- let root = top === null ? dir : fs5.realpathSync(top.trim());
2946
+ let root = top === null ? dir : fs6.realpathSync(top.trim());
2133
2947
  let lister = gitLister(root);
2134
2948
  let mode = "git";
2135
- const manifest = toPosix(path5.relative(root, path5.join(dir, "test-gates.json")));
2949
+ const manifest = toPosix(path6.relative(root, path6.join(dir, "test-gates.json")));
2136
2950
  if (top === null || !lister(dir, false).includes(manifest)) {
2137
2951
  mode = "walk";
2138
2952
  root = walkRoot(dir);
@@ -2140,17 +2954,17 @@ function snapshotOf(input) {
2140
2954
  }
2141
2955
  const listRoot = root;
2142
2956
  const list = lister;
2143
- const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path5.join(listRoot, file)));
2957
+ const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path6.join(listRoot, file)));
2144
2958
  const walk = walkLister(listRoot);
2145
2959
  const written = input.written.map((file) => {
2146
- const target = path5.resolve(dir, file);
2960
+ const target = path6.resolve(dir, file);
2147
2961
  try {
2148
- return fs5.realpathSync(target);
2962
+ return fs6.realpathSync(target);
2149
2963
  } catch {
2150
2964
  return target;
2151
2965
  }
2152
2966
  });
2153
- const swallowing = written.find((target) => isInside(target, path5.join(dir, "test-gates.json")));
2967
+ const swallowing = written.find((target) => isInside(target, path6.join(dir, "test-gates.json")));
2154
2968
  if (swallowing !== void 0) {
2155
2969
  throw new SnapshotError(`the run writes to ${swallowing}, which holds test-gates.json`);
2156
2970
  }
@@ -2167,12 +2981,12 @@ function snapshotOf(input) {
2167
2981
  if (!isInside(listRoot, directory)) {
2168
2982
  const written2 = reference === void 0 ? "" : ` with ${JSON.stringify(reference)}`;
2169
2983
  throw new SnapshotError(
2170
- `${toPosix(path5.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
2984
+ `${toPosix(path6.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
2171
2985
  );
2172
2986
  }
2173
2987
  if (directory === listRoot && listRoot !== dir && widened.length < WIDENED_SHOWN) {
2174
2988
  const written2 = reference === void 0 ? "" : ` ${JSON.stringify(reference)}`;
2175
- widened.push(`${toPosix(path5.relative(listRoot, from))}${written2}`);
2989
+ widened.push(`${toPosix(path6.relative(listRoot, from))}${written2}`);
2176
2990
  }
2177
2991
  if (![...queued].some((region) => isInside(region, directory))) {
2178
2992
  queued.add(directory);
@@ -2180,19 +2994,74 @@ function snapshotOf(input) {
2180
2994
  }
2181
2995
  };
2182
2996
  const follow = (fixed, open, anchored, file, reference) => {
2183
- const target = path5.resolve(path5.dirname(file), fixed);
2184
- let isDirectory = false;
2997
+ const from = (start) => {
2998
+ const target = path6.resolve(start, fixed);
2999
+ let isDirectory2 = false;
3000
+ try {
3001
+ isDirectory2 = fs6.statSync(target).isDirectory();
3002
+ } catch {
3003
+ }
3004
+ return {
3005
+ target,
3006
+ isDirectory: isDirectory2,
3007
+ found: isDirectory2 || resolveFile(target, true, false) !== null
3008
+ };
3009
+ };
3010
+ const fromFile = from(path6.dirname(file));
3011
+ const targets = [fromFile];
3012
+ if (!anchored && path6.dirname(file) !== dir) {
3013
+ const fromRun = from(dir);
3014
+ if (fromRun.found && !fromFile.found) {
3015
+ targets[0] = fromRun;
3016
+ } else if (fromRun.found && isInside(fromRun.target, listRoot)) {
3017
+ enqueue(listRoot, file, reference);
3018
+ } else if (fromRun.found) {
3019
+ targets.push(fromRun);
3020
+ }
3021
+ }
3022
+ for (const { target, isDirectory: isDirectory2 } of targets) {
3023
+ const beside = (isDirectory2 || open ? [] : resolveFiles(target, false, false)).filter(
3024
+ (one) => besideAncestors.has(one)
3025
+ );
3026
+ if (beside.length > 0) {
3027
+ beside.forEach(reach);
3028
+ continue;
3029
+ }
3030
+ if (!isDirectory2 && !isInside(listRoot, target) && resolveFile(target, true, false) === null) {
3031
+ files[toPosix(path6.relative(listRoot, target))] = "missing";
3032
+ continue;
3033
+ }
3034
+ const directory = isDirectory2 ? target : path6.dirname(target);
3035
+ if (fs6.existsSync(directory)) {
3036
+ enqueue(directory, file, reference, anchored && !open);
3037
+ }
3038
+ }
3039
+ };
3040
+ const reached = /* @__PURE__ */ new Set();
3041
+ const reach = (file) => {
3042
+ if (reached.has(file) || DOCUMENTS.includes(path6.extname(file).toLowerCase())) {
3043
+ return;
3044
+ }
3045
+ reached.add(file);
3046
+ let text;
2185
3047
  try {
2186
- isDirectory = fs5.statSync(target).isDirectory();
3048
+ const content = fs6.readFileSync(file);
3049
+ if (content.length > SCAN_LIMIT || content.subarray(0, 8e3).includes(0)) {
3050
+ return;
3051
+ }
3052
+ text = content.toString("utf8").replace(/\\[\\/]/g, "/");
2187
3053
  } catch {
2188
- }
2189
- if (!isDirectory && !isInside(listRoot, target) && resolveFile(target) === null) {
2190
- files[toPosix(path5.relative(listRoot, target))] = "missing";
2191
3054
  return;
2192
3055
  }
2193
- const directory = isDirectory ? target : path5.dirname(target);
2194
- if (fs5.existsSync(directory)) {
2195
- enqueue(directory, file, reference, anchored && !open);
3056
+ for (const match of text.matchAll(DOWNWARD)) {
3057
+ const literal = match[2];
3058
+ const open = literal.includes("${");
3059
+ const fixed = open ? literal.slice(0, literal.lastIndexOf("/", literal.indexOf("${")) + 1) : literal;
3060
+ if (open && isUpward(literal.slice(literal.indexOf("${")))) {
3061
+ enqueue(listRoot, file, literal);
3062
+ } else {
3063
+ follow(fixed, open, false, file, literal);
3064
+ }
2196
3065
  }
2197
3066
  };
2198
3067
  const scan = (text, file) => {
@@ -2208,7 +3077,7 @@ function snapshotOf(input) {
2208
3077
  const end = (match.index ?? 0) + match[0].length;
2209
3078
  const open = match[3] === void 0 || /^\s*[,+.}[]/.test(text.slice(end, end + 200));
2210
3079
  const reference = match[0].slice(0, match[0].length - (match[3]?.length ?? 0));
2211
- follow(path5.posix.join(...segments), open, true, file, reference);
3080
+ follow(path6.posix.join(...segments), open, true, file, reference);
2212
3081
  }
2213
3082
  for (const match of text.matchAll(UPWARD)) {
2214
3083
  const at = match.index ?? 0;
@@ -2228,70 +3097,91 @@ function snapshotOf(input) {
2228
3097
  }
2229
3098
  };
2230
3099
  const add = (relative) => {
2231
- const file = path5.join(listRoot, relative);
3100
+ const file = path6.join(listRoot, relative);
2232
3101
  if (relative in files || written.some((target) => isInside(target, file))) {
2233
3102
  return;
2234
3103
  }
2235
3104
  let stat;
2236
3105
  try {
2237
- stat = fs5.lstatSync(file);
3106
+ stat = fs6.lstatSync(file);
2238
3107
  } catch {
2239
3108
  files[relative] = "missing";
2240
3109
  return;
2241
3110
  }
2242
3111
  if (stat.isSymbolicLink()) {
2243
- files[relative] = `link ${fs5.readlinkSync(file)}`;
3112
+ files[relative] = `link ${fs6.readlinkSync(file)}`;
2244
3113
  let target;
2245
3114
  try {
2246
- target = fs5.realpathSync(file);
3115
+ target = fs6.realpathSync(file);
2247
3116
  } catch {
2248
3117
  return;
2249
3118
  }
2250
- enqueue(fs5.statSync(target).isDirectory() ? target : path5.dirname(target), file);
3119
+ enqueue(fs6.statSync(target).isDirectory() ? target : path6.dirname(target), file);
2251
3120
  } else if (stat.isDirectory()) {
2252
3121
  files[relative] = "directory";
2253
3122
  walk(file, true).forEach(add);
2254
3123
  } else if (!stat.isFile()) {
2255
3124
  files[relative] = "special";
2256
3125
  } else {
2257
- const content = fs5.readFileSync(file);
3126
+ const content = fs6.readFileSync(file);
2258
3127
  files[relative] = hashOf(content);
2259
- const document = DOCUMENTS.includes(path5.extname(file).toLowerCase());
3128
+ const document = DOCUMENTS.includes(path6.extname(file).toLowerCase());
2260
3129
  const loaded = followed === null || !isInside(dir, file) || followed.has(file);
2261
3130
  if (loaded && !document && content.length <= SCAN_LIMIT && !content.subarray(0, 8e3).includes(0)) {
2262
3131
  scan(content.toString("utf8").replace(/\\[\\/]/g, "/"), file);
2263
3132
  }
2264
3133
  }
2265
- };
2266
- const ancestors = upTo(dir, listRoot).slice(1);
2267
- for (const directory of ancestors) {
2268
- list(directory, false).forEach(add);
3134
+ };
3135
+ const ancestors = upTo(dir, listRoot).slice(1);
3136
+ const besideAncestors = /* @__PURE__ */ new Set();
3137
+ const ancestorFiles = ancestors.flatMap((directory) => list(directory, false));
3138
+ for (const relative of ancestorFiles) {
3139
+ besideAncestors.add(path6.join(listRoot, relative));
3140
+ }
3141
+ ancestorFiles.forEach(add);
3142
+ for (const directory of [dir, ...ancestors]) {
3143
+ const patches = path6.join(directory, "patches");
3144
+ if (fs6.existsSync(patches) && fs6.statSync(patches).isDirectory()) {
3145
+ enqueue(patches, directory);
3146
+ }
2269
3147
  }
2270
3148
  for (const directory of [dir, ...ancestors]) {
2271
- for (const linked of linkedPackages(path5.join(directory, "node_modules"), listRoot)) {
3149
+ for (const linked of linkedPackages(path6.join(directory, "node_modules"), listRoot)) {
2272
3150
  if (!isInside(linked, dir)) {
2273
- enqueue(linked, path5.join(directory, "node_modules"));
3151
+ enqueue(linked, path6.join(directory, "node_modules"));
2274
3152
  }
2275
3153
  }
2276
3154
  }
2277
3155
  const above = withinBounds(listRoot).slice(1);
2278
3156
  for (const directory of [dir, ...ancestors, ...above]) {
2279
3157
  const candidates = [
2280
- ...INSTALL_MARKERS.map((marker) => path5.join(directory, "node_modules", marker)),
2281
- ...above.includes(directory) ? LOCK_FILES.map((name) => path5.join(directory, name)) : []
3158
+ ...INSTALL_MARKERS.map((marker) => path6.join(directory, "node_modules", marker)),
3159
+ ...above.includes(directory) ? LOCK_FILES.map((name) => path6.join(directory, name)) : []
2282
3160
  ];
2283
3161
  for (const file of candidates) {
2284
3162
  const hash = hashFile(file);
2285
3163
  if (hash !== null) {
2286
- files[toPosix(path5.relative(listRoot, file))] = hash;
3164
+ files[toPosix(path6.relative(listRoot, file))] = hash;
2287
3165
  }
2288
3166
  }
2289
3167
  }
2290
- for (let position = 0; position < regions.length; position += 1) {
2291
- list(regions[position], true).forEach(add);
3168
+ let position = 0;
3169
+ const drain = () => {
3170
+ for (; position < regions.length; position += 1) {
3171
+ list(regions[position], true).forEach(add);
3172
+ }
3173
+ };
3174
+ drain();
3175
+ for (const file of followed ?? []) {
3176
+ const parts = path6.relative(listRoot, file).split(path6.sep);
3177
+ if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) && fs6.statSync(file, { throwIfNoEntry: false })?.isFile() === true) {
3178
+ add(toPosix(path6.relative(listRoot, file)));
3179
+ }
2292
3180
  }
3181
+ drain();
2293
3182
  const environment = {
2294
3183
  "Node.js": process.version,
3184
+ ...nodeOnPath(dir),
2295
3185
  platform: `${process.platform} ${process.arch}`,
2296
3186
  "test-gates": ownVersion()
2297
3187
  };
@@ -2305,12 +3195,26 @@ function snapshotOf(input) {
2305
3195
  }
2306
3196
 
2307
3197
  // src/affected-files.ts
2308
- import fs6 from "fs";
3198
+ import fs7 from "fs";
2309
3199
  import { builtinModules as builtinModules2 } from "module";
2310
- import path6 from "path";
3200
+ import path7 from "path";
2311
3201
  var AffectedError = class extends Error {
2312
3202
  };
2313
- var RUNTIME_PATHS = /\b(?:__dirname|__filename|require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readdirSync)\b|\bimport\.meta\.(?:url|dirname|filename|glob|resolve)\b/;
3203
+ var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readFile|readdirSync|fromFile|fromURL)\b|\bimport\.meta\.(?:glob|resolve)\b/;
3204
+ var PATH_FROM_TOP = /^[^\s./?#*:][^\s?#*:]*\.(?:html?|md|json)$/i;
3205
+ var withoutQuery = (specifier) => specifier.replace(/\?[^/]*$/, "");
3206
+ var RUNTIME_LOADS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire)\b|\bimport\.meta\.(?:glob|resolve)\b/;
3207
+ var LOCATION = /\b(?:__dirname|__filename)\b|\bimport\.meta\.(?:url|dirname|filename)\b/;
3208
+ var LOCATION_READS = /\b(?:readFile|readdir|readlink|existsSync|stat|statSync|lstat|lstatSync|access|accessSync|realpath|realpathSync|open|openSync|opendir|opendirSync|createReadStream|watch|watchFile|glob|globSync|promises)\b|\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
3209
+ var LOCATION_LOADS = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
3210
+ var LOCATION_SAFE = /* @__PURE__ */ new Set([
3211
+ "path",
3212
+ "path/posix",
3213
+ "path/win32",
3214
+ "url",
3215
+ "vitest",
3216
+ "@jest/globals"
3217
+ ]);
2314
3218
  var FILE_MODULES = /* @__PURE__ */ new Set([
2315
3219
  "fs",
2316
3220
  "fs/promises",
@@ -2323,30 +3227,31 @@ var FILE_MODULES = /* @__PURE__ */ new Set([
2323
3227
  "fast-glob",
2324
3228
  "globby"
2325
3229
  ]);
3230
+ var LOADING_MODULES = /* @__PURE__ */ new Set(["child_process", "module", "worker_threads"]);
2326
3231
  var bare = (specifier) => specifier.replace(/^node:/, "");
2327
3232
  function linkedPackage(specifier, from) {
2328
3233
  const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
2329
- for (let current = path6.dirname(from); ; current = path6.dirname(current)) {
2330
- const candidate = path6.join(current, "node_modules", name);
2331
- if (fs6.existsSync(candidate)) {
3234
+ for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
3235
+ const candidate = path7.join(current, "node_modules", name);
3236
+ if (fs7.existsSync(candidate)) {
2332
3237
  const target = realpath(candidate);
2333
- return target.split(path6.sep).includes("node_modules") ? null : target;
3238
+ return target.split(path7.sep).includes("node_modules") ? null : target;
2334
3239
  }
2335
- if (current === path6.dirname(current)) {
3240
+ if (current === path7.dirname(current)) {
2336
3241
  return null;
2337
3242
  }
2338
3243
  }
2339
3244
  }
2340
- var toPosix2 = (file) => file.split(path6.sep).join("/");
3245
+ var toPosix2 = (file) => file.split(path7.sep).join("/");
2341
3246
  function realpath(file) {
2342
3247
  try {
2343
- return fs6.realpathSync.native(file);
3248
+ return fs7.realpathSync.native(file);
2344
3249
  } catch {
2345
3250
  return file;
2346
3251
  }
2347
3252
  }
2348
3253
  function runnerConfigs(dir, stryker, options) {
2349
- const files = [path6.resolve(dir, stryker)];
3254
+ const files = [path7.resolve(dir, stryker)];
2350
3255
  const named = options.testRunner === "vitest" ? options.vitestConfig : options.jestConfig;
2351
3256
  if (options.testRunner !== "vitest" && options.testRunner !== "jest") {
2352
3257
  throw new AffectedError(
@@ -2354,129 +3259,325 @@ function runnerConfigs(dir, stryker, options) {
2354
3259
  );
2355
3260
  }
2356
3261
  if (typeof named === "string") {
2357
- files.push(path6.resolve(dir, named));
3262
+ files.push(path7.resolve(dir, named));
2358
3263
  } else {
2359
3264
  const names = options.testRunner === "vitest" ? ["vitest.config", "vite.config"] : ["jest.config"];
2360
3265
  for (const name of names) {
2361
3266
  for (const extension of [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]) {
2362
- const file = path6.join(dir, name + extension);
2363
- if (fs6.existsSync(file)) {
3267
+ const file = path7.join(dir, name + extension);
3268
+ if (fs7.existsSync(file)) {
2364
3269
  files.push(file);
2365
3270
  }
2366
3271
  }
2367
3272
  }
2368
3273
  }
2369
- return { files: files.filter((file) => fs6.existsSync(file)).map(realpath) };
3274
+ return {
3275
+ files: files.filter((file) => fs7.existsSync(file)).map(realpath),
3276
+ runner: options.testRunner,
3277
+ checkers: Array.isArray(options.checkers) && options.checkers.length > 0
3278
+ };
3279
+ }
3280
+ var CHECKS_TYPES = /\bcheckers\b|typescript-checker|\btypecheck\b/;
3281
+ var TS_JEST_STRIPS = /["']?(?:isolatedModules["']?\s*:\s*true|diagnostics["']?\s*:\s*false)/;
3282
+ var TS_JEST_UNSURE = /\b(?:projects|compilerOptions)\b|\btsconfig["']?\s*:\s*\{/;
3283
+ var STRIPPING_TRANSFORMS = /@swc\/jest|babel-jest|esbuild-jest|@sucrase\/jest/;
3284
+ var KEEPS_IMPORTS = /\b(?:verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues|onlyRemoveTypeImports)\b/;
3285
+ var COMPILER_CONFIG = /^(?:[jt]sconfig(?:\..+)?\.json|\.swcrc|\.babelrc(?:\..+)?|babel\.config\..+)$/;
3286
+ function typesErased(configs, texts, compiler) {
3287
+ const all = texts.map(withoutComments).join("\n");
3288
+ let erased = false;
3289
+ if (configs.checkers !== true && !CHECKS_TYPES.test(all)) {
3290
+ if (configs.runner === "vitest") {
3291
+ erased = true;
3292
+ } else if (configs.runner === "jest") {
3293
+ erased = /\bts-jest\b/.test(all) ? TS_JEST_STRIPS.test(all) && !TS_JEST_UNSURE.test(all) : STRIPPING_TRANSFORMS.test(all) && !/\bpreset\b/.test(all);
3294
+ }
3295
+ }
3296
+ const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(path7.basename(file))) && compiler.every(({ text }) => text !== null && !KEEPS_IMPORTS.test(text));
3297
+ return { erased, inline };
3298
+ }
3299
+ function compilerConfigs(dir, root, keys) {
3300
+ const above = /* @__PURE__ */ new Set();
3301
+ for (let current = path7.dirname(dir); ; current = path7.dirname(current)) {
3302
+ const relative = path7.relative(root, current);
3303
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) {
3304
+ break;
3305
+ }
3306
+ above.add(current);
3307
+ if (current === path7.dirname(current)) {
3308
+ break;
3309
+ }
3310
+ }
3311
+ const compared = new Set(keys.map((key) => path7.join(root, key)));
3312
+ const starts = [...compared].filter(
3313
+ (file) => (COMPILER_CONFIG.test(path7.basename(file)) || path7.basename(file) === "package.json") && (file.startsWith(`${dir}${path7.sep}`) || above.has(path7.dirname(file)))
3314
+ );
3315
+ const found = [];
3316
+ const seen = /* @__PURE__ */ new Set();
3317
+ const queue = [...starts];
3318
+ for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
3319
+ if (seen.has(file)) {
3320
+ continue;
3321
+ }
3322
+ seen.add(file);
3323
+ let text = null;
3324
+ if (compared.has(file) || file.split(path7.sep).includes("node_modules")) {
3325
+ try {
3326
+ text = fs7.readFileSync(file, "utf8");
3327
+ } catch {
3328
+ text = null;
3329
+ }
3330
+ }
3331
+ found.push({ file, text });
3332
+ for (const base of text === null ? [] : valuesOf(text, "extends")) {
3333
+ queue.push(extended(base, file) ?? `${file} extends ${base}`);
3334
+ }
3335
+ }
3336
+ return found;
3337
+ }
3338
+ function extended(base, from) {
3339
+ const candidates = (start) => [
3340
+ start,
3341
+ `${start}.json`,
3342
+ path7.join(start, "tsconfig.json")
3343
+ ];
3344
+ if (base.startsWith(".") || path7.isAbsolute(base)) {
3345
+ return candidates(path7.resolve(path7.dirname(from), base)).find(isFile2) ?? null;
3346
+ }
3347
+ for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
3348
+ const file = candidates(path7.join(current, "node_modules", base)).find(isFile2);
3349
+ if (file !== void 0) {
3350
+ return file;
3351
+ }
3352
+ if (current === path7.dirname(current)) {
3353
+ return null;
3354
+ }
3355
+ }
3356
+ }
3357
+ function isFile2(file) {
3358
+ try {
3359
+ return fs7.statSync(file).isFile();
3360
+ } catch {
3361
+ return false;
3362
+ }
2370
3363
  }
2371
3364
  function fileEdges(file, context) {
2372
- const { dir, aliases, cache } = context;
3365
+ const { dir, aliases, cache, types } = context;
2373
3366
  const known = cache.get(file);
2374
3367
  if (known !== void 0) {
2375
3368
  return known;
2376
3369
  }
2377
- const shown = (one) => toPosix2(path6.relative(dir, one));
3370
+ const shown = (one) => toPosix2(path7.relative(dir, one));
2378
3371
  const target = (specifier, from) => {
2379
3372
  if (specifier.startsWith(".")) {
2380
- return resolveFile(path6.resolve(path6.dirname(from), specifier));
3373
+ return resolveFiles(path7.resolve(path7.dirname(from), specifier));
2381
3374
  }
2382
3375
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
2383
- return alias === void 0 ? null : resolveFile(path6.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
3376
+ return alias === void 0 ? [] : resolveFiles(path7.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
3377
+ };
3378
+ const found = { loads: [], unknown: null, loading: null, computed: false, unnamed: [] };
3379
+ const loading = (reason) => {
3380
+ found.unknown ??= reason;
3381
+ found.loading ??= reason;
2384
3382
  };
2385
- const found = { loads: [], unknown: null, computed: false, unnamed: [] };
2386
3383
  cache.set(file, found);
2387
- const snapshotFile = path6.join(
2388
- path6.dirname(file),
3384
+ const snapshotFile = path7.join(
3385
+ path7.dirname(file),
2389
3386
  "__snapshots__",
2390
- `${path6.basename(file)}.snap`
3387
+ `${path7.basename(file)}.snap`
2391
3388
  );
2392
- if (fs6.existsSync(snapshotFile)) {
3389
+ if (fs7.existsSync(snapshotFile)) {
2393
3390
  found.loads.push(realpath(snapshotFile));
2394
3391
  }
3392
+ const mocks = path7.join(path7.dirname(file), "__mocks__");
3393
+ const name = path7.basename(file);
3394
+ const candidates = isDirectory(mocks) ? [
3395
+ path7.join(mocks, name),
3396
+ ...resolveFiles(path7.join(mocks, name.replace(/\.[^.]+$/, "")), false, false)
3397
+ ] : [];
3398
+ for (const mock of new Set(candidates)) {
3399
+ if (isFile2(mock)) {
3400
+ found.loads.push(realpath(mock));
3401
+ }
3402
+ }
2395
3403
  if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
2396
3404
  return found;
2397
3405
  }
2398
- const source = fs6.readFileSync(file, "utf8");
3406
+ const source = fs7.readFileSync(file, "utf8");
3407
+ const code = withoutComments(source);
2399
3408
  if (COMPONENT.test(file)) {
2400
- found.unknown = `${shown(file)} is a component, which can use others that it does not import`;
3409
+ loading(`${shown(file)} is a component, which can use others that it does not import`);
2401
3410
  }
2402
- const uncheckable = findUncheckableImports(source)[0];
2403
- const runtime = RUNTIME_PATHS.exec(source);
3411
+ const uncheckable = findUncheckableImports(code)[0];
3412
+ const runtime = RUNTIME_PATHS.exec(code);
3413
+ const runtimeLoads = RUNTIME_LOADS.exec(code);
2404
3414
  if (uncheckable !== void 0) {
2405
3415
  found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
3416
+ found.loading = found.unknown;
2406
3417
  found.computed = true;
2407
3418
  } else if (runtime !== null) {
2408
3419
  found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
3420
+ if (runtimeLoads !== null) {
3421
+ found.loading ??= `${shown(file)} uses ${runtimeLoads[0]}, which can reach files that no import names`;
3422
+ }
2409
3423
  }
3424
+ const location = LOCATION.exec(code);
3425
+ const refs = parseImports(code);
3426
+ const dropped = (ref) => types !== void 0 && types.erased && (ref.typeOnly || types.inline && ref.inlineTypes === true);
2410
3427
  const specifiers = /* @__PURE__ */ new Set([
2411
- ...parseImports(source).map((ref) => ref.module),
2412
- ...[...source.matchAll(RUNNER_LOAD)].map((match) => match[2])
3428
+ ...refs.filter((ref) => !dropped(ref)).map((ref) => ref.module),
3429
+ ...[...code.matchAll(RUNNER_LOAD)].map((match) => match[2])
2413
3430
  ]);
3431
+ const typeLines = /* @__PURE__ */ new Map();
3432
+ for (const ref of refs.filter(dropped)) {
3433
+ const key = `${ref.line}:${ref.module}`;
3434
+ typeLines.set(key, (typeLines.get(key) ?? 0) + 1);
3435
+ }
2414
3436
  for (const specifier of specifiers) {
2415
- if (path6.isAbsolute(specifier) || specifier.startsWith("file:")) {
2416
- const resolved = resolveFile(
2417
- specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
3437
+ if (path7.isAbsolute(specifier) || specifier.startsWith("file:")) {
3438
+ const resolved2 = resolveFiles(
3439
+ withoutQuery(specifier.startsWith("file:") ? new URL(specifier).pathname : specifier)
2418
3440
  );
2419
- if (resolved === null) {
2420
- found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
3441
+ if (resolved2.length === 0) {
3442
+ loading(`${shown(file)} imports ${specifier}, which leads to no file`);
2421
3443
  } else {
2422
- found.loads.push(realpath(resolved));
3444
+ found.loads.push(...resolved2.map(realpath));
2423
3445
  }
2424
3446
  } else if (isProjectImport(specifier, aliases)) {
2425
- const resolved = target(specifier, file);
2426
- if (resolved === null) {
2427
- found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
3447
+ const resolved2 = target(withoutQuery(specifier), file);
3448
+ if (resolved2.length === 0) {
3449
+ loading(`${shown(file)} imports ${specifier}, which leads to no file`);
2428
3450
  } else {
2429
- found.loads.push(realpath(resolved));
3451
+ found.loads.push(...resolved2.map(realpath));
2430
3452
  }
2431
3453
  } else if (FILE_MODULES.has(bare(specifier))) {
2432
- found.unknown ??= `${shown(file)} imports ${specifier}, which can reach files that no import names`;
3454
+ const reason = `${shown(file)} imports ${specifier}, which can reach files that no import names`;
3455
+ if (LOADING_MODULES.has(bare(specifier))) {
3456
+ loading(reason);
3457
+ } else {
3458
+ found.unknown ??= reason;
3459
+ }
2433
3460
  } else if (!specifier.startsWith("node:") && !builtinModules2.includes(specifier)) {
2434
3461
  if (!installed(specifier, file)) {
2435
- found.unknown ??= `${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`;
3462
+ loading(
3463
+ `${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`
3464
+ );
2436
3465
  found.unnamed.push(specifier);
2437
3466
  } else if (linkedPackage(specifier, file) !== null) {
2438
- found.unknown ??= `${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`;
3467
+ loading(
3468
+ `${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`
3469
+ );
2439
3470
  found.computed = true;
2440
3471
  }
2441
3472
  }
2442
3473
  }
3474
+ if (location !== null) {
3475
+ const reads = LOCATION_READS.exec(code);
3476
+ const reader = [...specifiers].find(
3477
+ (specifier) => !specifier.startsWith(".") && !path7.isAbsolute(specifier) && !specifier.startsWith("file:") && !isProjectImport(specifier, aliases) && !LOCATION_SAFE.has(bare(specifier))
3478
+ );
3479
+ if (reads !== null) {
3480
+ found.unknown ??= `${shown(file)} uses ${location[0]} and ${reads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
3481
+ const loads = LOCATION_LOADS.exec(code);
3482
+ if (loads !== null) {
3483
+ found.loading ??= `${shown(file)} uses ${location[0]} and ${loads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
3484
+ }
3485
+ } else if (reader !== void 0) {
3486
+ found.unknown ??= `${shown(file)} uses ${location[0]} and imports ${reader}, which can read files by it that no import names`;
3487
+ }
3488
+ }
3489
+ let line = 1;
3490
+ let counted = 0;
2443
3491
  for (const match of source.matchAll(QUOTED)) {
2444
- const text = match[2];
2445
- if (text.startsWith(".") && !specifiers.has(text)) {
2446
- const resolved = resolveFile(path6.resolve(path6.dirname(file), text));
2447
- if (resolved !== null) {
2448
- found.loads.push(realpath(resolved));
3492
+ for (; counted < match.index; counted += 1) {
3493
+ if (source[counted] === "\n") {
3494
+ line += 1;
2449
3495
  }
2450
3496
  }
3497
+ const text = match[2];
3498
+ const key = `${line}:${text}`;
3499
+ const left = typeLines.get(key) ?? 0;
3500
+ if (left > 0) {
3501
+ typeLines.set(key, left - 1);
3502
+ } else if (text.startsWith(".") && !specifiers.has(text)) {
3503
+ found.loads.push(...resolveFiles(path7.resolve(path7.dirname(file), text)).map(realpath));
3504
+ } else if (PATH_FROM_TOP.test(text) && isFile2(path7.resolve(dir, text))) {
3505
+ found.loads.push(realpath(path7.resolve(dir, text)));
3506
+ }
2451
3507
  }
2452
3508
  return found;
2453
3509
  }
2454
3510
  function affectedInputs(manifest, input) {
3511
+ return resolving(() => inputsOf(manifest, input));
3512
+ }
3513
+ function inputsOf(manifest, input) {
2455
3514
  const dir = realpath(manifest.dir);
2456
3515
  const aliases = manifest.settings.importAliases;
2457
3516
  const placed = [...input.tests, ...Object.values(input.covering).flat()];
2458
- const absolute = placed.find((file) => path6.isAbsolute(file) || file.startsWith(".."));
3517
+ const absolute = placed.find((file) => path7.isAbsolute(file) || file.startsWith(".."));
2459
3518
  if (absolute !== void 0) {
2460
3519
  throw new AffectedError(`the report names a spec file outside of the subproject: ${absolute}`);
2461
3520
  }
2462
3521
  const keyOf2 = (file) => {
2463
- const relative = path6.relative(input.root, realpath(file));
2464
- if (relative.startsWith("..") || path6.isAbsolute(relative)) {
3522
+ const relative = path7.relative(input.root, realpath(file));
3523
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) {
2465
3524
  throw new AffectedError(`${file} is outside of the files that are compared`);
2466
3525
  }
2467
3526
  return toPosix2(relative);
2468
3527
  };
2469
- const shown = (file) => toPosix2(path6.relative(dir, file));
3528
+ const shown = (file) => toPosix2(path7.relative(dir, file));
3529
+ const plain2 = /* @__PURE__ */ new Map();
3530
+ const configTexts = [];
3531
+ const runnerSources = [];
3532
+ const reached = new Set(input.configs.files);
3533
+ const pending = [...input.configs.files];
3534
+ for (let file = pending.shift(); file !== void 0; file = pending.shift()) {
3535
+ const text = fs7.readFileSync(file, "utf8");
3536
+ configTexts.push(text);
3537
+ runnerSources.push({ file, text });
3538
+ for (const next of fileEdges(file, { dir, aliases, cache: plain2 }).loads) {
3539
+ if (!reached.has(next) && SCRIPT.test(next)) {
3540
+ reached.add(next);
3541
+ pending.push(next);
3542
+ }
3543
+ }
3544
+ }
3545
+ const compiler = compilerConfigs(dir, realpath(input.root), Object.keys(input.files));
3546
+ const types = typesErased(input.configs, configTexts, compiler);
3547
+ const maps = unexplainedMaps({
3548
+ dir,
3549
+ aliases,
3550
+ // Not what lies outside of the compared files: this package, imported by path, is installed.
3551
+ runner: [
3552
+ ...runnerSources.filter(({ file }) => {
3553
+ const relative = path7.relative(input.root, realpath(file));
3554
+ return !relative.startsWith("..") && toPosix2(relative) in input.files;
3555
+ }),
3556
+ ...jestInPackage(dir, input.configs)
3557
+ ],
3558
+ compiler,
3559
+ ...input.configs.runner === void 0 ? {} : { testRunner: input.configs.runner }
3560
+ });
3561
+ if (maps !== null) {
3562
+ throw new AffectedError(maps);
3563
+ }
2470
3564
  const edges = /* @__PURE__ */ new Map();
2471
- const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges });
3565
+ const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges, types });
3566
+ const blind = /* @__PURE__ */ Object.create(null);
2472
3567
  const closure = (start, strict) => {
2473
3568
  const seen = /* @__PURE__ */ new Set([start]);
2474
3569
  const queue = [start];
2475
3570
  for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
2476
3571
  const found = edgesOf(file);
2477
- if (strict && found.unknown !== null) {
3572
+ if (strict === "every" && found.unknown !== null) {
2478
3573
  throw new AffectedError(found.unknown);
2479
3574
  }
3575
+ if (strict === "gate" && found.unknown !== null) {
3576
+ if (found.loading !== null) {
3577
+ throw new AffectedError(found.loading);
3578
+ }
3579
+ blind[keyOf2(file)] = found.unknown;
3580
+ }
2480
3581
  for (const next of found.loads) {
2481
3582
  if (!seen.has(next)) {
2482
3583
  seen.add(next);
@@ -2487,8 +3588,8 @@ function affectedInputs(manifest, input) {
2487
3588
  return [...seen];
2488
3589
  };
2489
3590
  const isCompared = (file) => {
2490
- const relative = path6.relative(input.root, realpath(file));
2491
- return !relative.startsWith("..") && !path6.isAbsolute(relative) && toPosix2(relative) in input.files;
3591
+ const relative = path7.relative(input.root, realpath(file));
3592
+ return !relative.startsWith("..") && !path7.isAbsolute(relative) && toPosix2(relative) in input.files;
2492
3593
  };
2493
3594
  const keysOf = (files) => files.map((file) => {
2494
3595
  const key = keyOf2(file);
@@ -2507,17 +3608,25 @@ function affectedInputs(manifest, input) {
2507
3608
  if (!SCRIPT.test(loaded)) {
2508
3609
  continue;
2509
3610
  }
2510
- for (const match of fs6.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
3611
+ for (const match of fs7.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
2511
3612
  const text = match[2].replace(/^<rootDir>\//, "");
2512
- const found = resolveFile(path6.resolve(path6.dirname(loaded), text), false) ?? resolveFile(path6.resolve(dir, text), false);
2513
- if (found !== null && !text.includes("*") && isCompared(found)) {
2514
- named.push(realpath(found));
3613
+ const here = resolveFiles(path7.resolve(path7.dirname(loaded), text), false, false);
3614
+ const candidates = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false, false);
3615
+ for (const found of candidates) {
3616
+ if (text.includes("*") || !isCompared(found)) {
3617
+ continue;
3618
+ }
3619
+ if (SCRIPT.test(found)) {
3620
+ named.push(realpath(found));
3621
+ } else {
3622
+ config.add(realpath(found));
3623
+ }
2515
3624
  }
2516
3625
  }
2517
3626
  }
2518
3627
  }
2519
3628
  const gatePaths = new Map(
2520
- manifest.gates.map((gate) => [realpath(path6.join(dir, gate.path)), gate.path])
3629
+ manifest.gates.map((gate) => [realpath(path7.join(dir, gate.path)), gate.path])
2521
3630
  );
2522
3631
  const tests = [
2523
3632
  .../* @__PURE__ */ new Set([
@@ -2528,13 +3637,13 @@ function affectedInputs(manifest, input) {
2528
3637
  ];
2529
3638
  const runAsSuch = new Set(
2530
3639
  [...tests, ...manifest.gates.map((gate) => gate.path)].map(
2531
- (file) => realpath(path6.join(dir, file))
3640
+ (file) => realpath(path7.join(dir, file))
2532
3641
  )
2533
3642
  );
2534
- const subprojectKey = toPosix2(path6.relative(input.root, dir));
3643
+ const subprojectKey = toPosix2(path7.relative(input.root, dir));
2535
3644
  const isConfig = (file) => input.configs.files.includes(file) || kindOf(keyOf2(file), subprojectKey) === "config";
2536
3645
  for (const file of named.filter((one) => !isConfig(one) && !runAsSuch.has(one))) {
2537
- for (const loaded of closure(file, true)) {
3646
+ for (const loaded of closure(file, "every")) {
2538
3647
  const gate = gatePaths.get(loaded);
2539
3648
  if (gate !== void 0) {
2540
3649
  throw new AffectedError(
@@ -2544,16 +3653,16 @@ function affectedInputs(manifest, input) {
2544
3653
  config.add(loaded);
2545
3654
  }
2546
3655
  }
2547
- const fromSubproject = (file) => path6.join(dir, file);
3656
+ const fromSubproject = (file) => path7.join(dir, file);
2548
3657
  const closures = /* @__PURE__ */ Object.create(null);
2549
3658
  for (const file of [...manifest.gates.map((gate) => gate.path), ...tests]) {
2550
3659
  const absolute2 = fromSubproject(file);
2551
- if (!fs6.existsSync(absolute2)) {
3660
+ if (!fs7.existsSync(absolute2)) {
2552
3661
  continue;
2553
3662
  }
2554
- closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), true));
3663
+ closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), "gate"));
2555
3664
  }
2556
- const keyed = (files) => files.map((file) => toPosix2(path6.relative(input.root, path6.join(dir, file))));
3665
+ const keyed = (files) => files.map((file) => toPosix2(path7.relative(input.root, path7.join(dir, file))));
2557
3666
  const dependencies = gateDependencies({
2558
3667
  gates: manifest.gates.map((gate) => ({
2559
3668
  key: keyed([gate.path])[0],
@@ -2563,9 +3672,10 @@ function affectedInputs(manifest, input) {
2563
3672
  covering: Object.fromEntries(
2564
3673
  Object.entries(input.covering).map(([gate, specs]) => [keyed([gate])[0], keyed(specs)])
2565
3674
  ),
2566
- closures
3675
+ closures,
3676
+ blind
2567
3677
  });
2568
- const subproject = toPosix2(path6.relative(input.root, dir));
3678
+ const subproject = toPosix2(path7.relative(input.root, dir));
2569
3679
  const prefix = subproject === "" ? "" : `${subproject}/`;
2570
3680
  const gates = /* @__PURE__ */ Object.create(null);
2571
3681
  for (const gate of manifest.gates) {
@@ -2576,10 +3686,26 @@ function affectedInputs(manifest, input) {
2576
3686
  specs: found.specs.map(
2577
3687
  (spec) => spec.startsWith(prefix) ? spec.slice(prefix.length) : spec
2578
3688
  ),
2579
- files: found.files
3689
+ files: found.files,
3690
+ ...found.blind === void 0 ? {} : { blind: found.blind }
2580
3691
  };
2581
3692
  }
2582
- return { subproject, config: keysOf([...config]).sort(), gates };
3693
+ const packageMocks = Object.keys(input.files).filter(
3694
+ (key) => isNestedMock(key, subproject) && mockNames(key).some(
3695
+ (name) => builtinModules2.includes(name) || installed(name, path7.join(input.root, key))
3696
+ )
3697
+ );
3698
+ return {
3699
+ subproject,
3700
+ config: [.../* @__PURE__ */ new Set([...keysOf([...config]), ...packageMocks])].sort(),
3701
+ gates
3702
+ };
3703
+ }
3704
+ function mockNames(key) {
3705
+ const segments = key.replace(/\.[^./]+$/, "").split("/");
3706
+ return segments.flatMap(
3707
+ (segment, index) => segment === "__mocks__" && index < segments.length - 1 ? [segments.slice(index + 1).join("/")] : []
3708
+ );
2583
3709
  }
2584
3710
  var NOT_THE_HELPER2 = "the runner config is not made with createJestGatesConfig or createVitestGatesConfig";
2585
3711
  var OWN = /@t09tanaka\/test-gates\/(?:jest|vitest)\b/;
@@ -2615,11 +3741,14 @@ var READ_BY_NAME = [
2615
3741
  /^\.swcrc$/
2616
3742
  ];
2617
3743
  function loadedScope(manifest, configs, listed2) {
3744
+ return resolving(() => scopeOf(manifest, configs, listed2));
3745
+ }
3746
+ function scopeOf(manifest, configs, listed2) {
2618
3747
  const dir = realpath(manifest.dir);
2619
3748
  const cache = /* @__PURE__ */ new Map();
2620
3749
  const context = { dir, aliases: manifest.settings.importAliases, cache };
2621
3750
  const runner = configs.files.slice(1);
2622
- const texts = runner.map((file) => withoutComments(fs6.readFileSync(file, "utf8")));
3751
+ const texts = runner.map((file) => withoutComments(fs7.readFileSync(file, "utf8")));
2623
3752
  if (runner.length === 0 || !texts.every((text) => OWN.test(text))) {
2624
3753
  return { every: NOT_THE_HELPER2 };
2625
3754
  }
@@ -2634,21 +3763,21 @@ function loadedScope(manifest, configs, listed2) {
2634
3763
  fileEdges(file, context).loads.forEach((loaded) => parts.add(loaded));
2635
3764
  }
2636
3765
  }
2637
- if ([...parts].some((file) => PROJECTS.test(withoutComments(fs6.readFileSync(file, "utf8"))))) {
3766
+ if ([...parts].some((file) => PROJECTS.test(withoutComments(fs7.readFileSync(file, "utf8"))))) {
2638
3767
  return {
2639
3768
  every: "the runner config, or a file it is put together from, names projects or a workspace"
2640
3769
  };
2641
3770
  }
2642
- const named = fs6.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path6.join(dir, name));
2643
- const mocks = path6.join(dir, "__mocks__");
3771
+ const named = fs7.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path7.join(dir, name));
3772
+ const mocks = path7.join(dir, "__mocks__");
2644
3773
  const seeds = [
2645
3774
  ...configs.files,
2646
3775
  ...named,
2647
- ...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path6.join(dir, file)),
2648
- ...listed2.filter((file) => file.startsWith(`${mocks}${path6.sep}`))
2649
- ].filter((file) => fs6.existsSync(file)).map(realpath);
3776
+ ...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path7.join(dir, file)),
3777
+ ...listed2.filter((file) => file.startsWith(`${mocks}${path7.sep}`))
3778
+ ].filter((file) => fs7.existsSync(file)).map(realpath);
2650
3779
  const naming = new Set(
2651
- [...configs.files, ...named].filter((file) => fs6.existsSync(file)).map(realpath)
3780
+ [...configs.files, ...named].filter((file) => fs7.existsSync(file)).map(realpath)
2652
3781
  );
2653
3782
  const seen = new Set(seeds);
2654
3783
  const queue = [...seeds];
@@ -2660,7 +3789,7 @@ function loadedScope(manifest, configs, listed2) {
2660
3789
  }
2661
3790
  };
2662
3791
  for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
2663
- if (fs6.statSync(file).isDirectory()) {
3792
+ if (fs7.statSync(file).isDirectory()) {
2664
3793
  continue;
2665
3794
  }
2666
3795
  const edges = fileEdges(file, context);
@@ -2671,19 +3800,16 @@ function loadedScope(manifest, configs, listed2) {
2671
3800
  for (const specifier of edges.unnamed) {
2672
3801
  endingAs(specifier, listed2).forEach(add);
2673
3802
  }
2674
- const mock = path6.join(path6.dirname(file), "__mocks__", path6.basename(file));
2675
- if (fs6.existsSync(mock)) {
2676
- add(mock);
2677
- }
2678
- if (naming.has(file) || SCRIPT.test(file) && path6.dirname(file) === dir) {
3803
+ if (naming.has(file) || SCRIPT.test(file) && path7.dirname(file) === dir) {
2679
3804
  for (const named2 of namedIn(file)) {
2680
3805
  const text = named2.replace(/^<rootDir>\//, "");
2681
- const found = resolveFile(path6.resolve(path6.dirname(file), text), false) ?? resolveFile(path6.resolve(dir, text), false);
2682
- if (found !== null && !text.includes("*")) {
2683
- if (!SCRIPT.test(found)) {
2684
- naming.add(realpath(found));
3806
+ const here = resolveFiles(path7.resolve(path7.dirname(file), text), false);
3807
+ const found = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false);
3808
+ for (const one of text.includes("*") ? [] : found) {
3809
+ if (!SCRIPT.test(one)) {
3810
+ naming.add(realpath(one));
2685
3811
  }
2686
- add(found);
3812
+ add(one);
2687
3813
  }
2688
3814
  }
2689
3815
  }
@@ -2702,7 +3828,7 @@ function endingAs(specifier, files) {
2702
3828
  });
2703
3829
  }
2704
3830
  function namedIn(file) {
2705
- const text = fs6.readFileSync(file, "utf8");
3831
+ const text = fs7.readFileSync(file, "utf8");
2706
3832
  if (SCRIPT.test(file)) {
2707
3833
  return [...text.matchAll(QUOTED)].map((match) => match[2]);
2708
3834
  }
@@ -2740,298 +3866,475 @@ function valuesOf(text, key) {
2740
3866
  return found;
2741
3867
  }
2742
3868
  function withoutComments(text) {
2743
- let out = "";
3869
+ const out = [];
3870
+ const before = { last: "", word: "" };
3871
+ let space = false;
3872
+ const keep = (part) => {
3873
+ out.push(part);
3874
+ for (const one of part) {
3875
+ if (/\s/.test(one)) {
3876
+ space = true;
3877
+ continue;
3878
+ }
3879
+ const word = /[\w$]/.test(one);
3880
+ before.word = !word ? "" : space || !/[\w$]/.test(before.last) ? one : before.word + one;
3881
+ before.last = one;
3882
+ space = false;
3883
+ }
3884
+ };
2744
3885
  for (let at = 0; at < text.length; at += 1) {
2745
3886
  const char = text[at];
3887
+ const next = text[at + 1];
2746
3888
  if (char === "'" || char === '"' || char === "`") {
2747
- let end = at + 1;
2748
- while (end < text.length && text[end] !== char && text[end] !== "\n") {
2749
- end += text[end] === "\\" ? 2 : 1;
2750
- }
2751
- out += text.slice(at, end + 1);
3889
+ const end = stringEnd(text, at);
3890
+ keep(text.slice(at, end + 1));
2752
3891
  at = end;
2753
- } else if (char === "/" && text[at + 1] === "/") {
3892
+ } else if (char === "/" && next === "/" && !SCHEME.test(text.slice(Math.max(0, at - 40), at))) {
2754
3893
  const end = text.indexOf("\n", at);
2755
3894
  at = end === -1 ? text.length : end - 1;
2756
- } else if (char === "/" && text[at + 1] === "*") {
3895
+ } else if (char === "/" && next === "*") {
2757
3896
  const end = text.indexOf("*/", at + 2);
2758
- at = end === -1 ? text.length : end + 1;
3897
+ const stop = end === -1 ? text.length : end + 2;
3898
+ keep(` ${text.slice(at, stop).replace(/[^\n]/g, "")}`);
3899
+ at = stop - 1;
3900
+ } else if (char === "/" && next !== "/") {
3901
+ const end = patternEnd(text, at, mayStartPattern(before));
3902
+ keep(text.slice(at, end + 1));
3903
+ at = end;
3904
+ } else {
3905
+ keep(char);
3906
+ }
3907
+ }
3908
+ return out.join("");
3909
+ }
3910
+ var SCHEME = /(?:^|[^\w$.+-])[A-Za-z][\w+.-]*:$/;
3911
+ function patternEnd(text, at, pattern) {
3912
+ let inClass = false;
3913
+ let end = at + 1;
3914
+ for (; end < text.length && text[end] !== "\n"; end += 1) {
3915
+ const char = text[end];
3916
+ if (char === "\\") {
3917
+ end += 1;
3918
+ } else if (char === "[") {
3919
+ inClass = true;
3920
+ } else if (char === "]") {
3921
+ inClass = false;
3922
+ } else if (char === "/" && !inClass) {
3923
+ const comment = text[end + 1] === "/" || text[end + 1] === "*";
3924
+ return pattern || !comment ? end : at;
3925
+ } else if (!pattern && (char === "'" || char === '"' || char === "`")) {
3926
+ return at;
3927
+ }
3928
+ }
3929
+ return pattern ? end : at;
3930
+ }
3931
+ function stringEnd(text, at) {
3932
+ const quote = text[at];
3933
+ let end = at + 1;
3934
+ while (end < text.length && text[end] !== quote && (quote === "`" || text[end] !== "\n")) {
3935
+ if (text[end] === "\\") {
3936
+ end += 2;
3937
+ } else if (quote === "`" && text.startsWith("${", end)) {
3938
+ end = placeholderEnd(text, end + 2) + 1;
2759
3939
  } else {
2760
- out += char;
3940
+ end += 1;
2761
3941
  }
2762
3942
  }
2763
- return out;
2764
- }
2765
-
2766
- // src/commands/mutation.ts
2767
- import { spawnSync as spawnSync2 } from "child_process";
2768
- import fs8 from "fs";
2769
- import path8 from "path";
2770
- import { pathToFileURL } from "url";
2771
-
2772
- // src/commands/mutation-result.ts
2773
- import fs7 from "fs";
2774
- import path7 from "path";
2775
-
2776
- // src/core/mutation-result.ts
2777
- var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
2778
- var SURVIVED = /* @__PURE__ */ new Set(["Survived", "NoCoverage"]);
2779
- var NOT_EVALUABLE = /* @__PURE__ */ new Set(["RuntimeError", "CompileError"]);
2780
- function normalizeCode(text) {
2781
- return text.replace(/\s+/g, " ").trim();
2782
- }
2783
- function normalizePath(file) {
2784
- return file.replace(/\\/g, "/").replace(/^\.\//, "");
3943
+ return end;
2785
3944
  }
2786
- function normalizeNewlines(text) {
2787
- return text.replace(/\r\n/g, "\n");
3945
+ function placeholderEnd(text, at) {
3946
+ let depth = 0;
3947
+ for (let index = at; index < text.length; index += 1) {
3948
+ const char = text[index];
3949
+ if (char === "'" || char === '"' || char === "`") {
3950
+ index = stringEnd(text, index);
3951
+ } else if (char === "{") {
3952
+ depth += 1;
3953
+ } else if (char === "}") {
3954
+ if (depth === 0) {
3955
+ return index;
3956
+ }
3957
+ depth -= 1;
3958
+ }
3959
+ }
3960
+ return text.length;
3961
+ }
3962
+ var BEFORE_PATTERN = /* @__PURE__ */ new Set([
3963
+ "return",
3964
+ "typeof",
3965
+ "instanceof",
3966
+ "in",
3967
+ "of",
3968
+ "new",
3969
+ "delete",
3970
+ "void",
3971
+ "throw",
3972
+ "case",
3973
+ "do",
3974
+ "else",
3975
+ "yield",
3976
+ "await"
3977
+ ]);
3978
+ function mayStartPattern(before) {
3979
+ if (before.word !== "") {
3980
+ return BEFORE_PATTERN.has(before.word);
3981
+ }
3982
+ return before.last === "" || !")]".includes(before.last);
3983
+ }
3984
+ function jestInPackage(dir, configs) {
3985
+ const file = path7.join(dir, "package.json");
3986
+ return configs.runner === "jest" && configs.files.length === 1 && fs7.existsSync(file) ? [{ file, text: fs7.readFileSync(file, "utf8"), mapperOnly: true }] : [];
3987
+ }
3988
+ var STRING_LITERAL = String.raw`'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"`;
3989
+ function stringValue(text) {
3990
+ const match = new RegExp(`^\\s*(${STRING_LITERAL})\\s*$`).exec(text);
3991
+ return match === null ? null : match[1].slice(1, -1).replace(/\\(.)/g, "$1");
3992
+ }
3993
+ function partsOf2(text, at) {
3994
+ const parts = [];
3995
+ let depth = 0;
3996
+ let start = at + 1;
3997
+ for (let index = at; index < text.length; index += 1) {
3998
+ const char = text[index];
3999
+ if (char === "'" || char === '"' || char === "`") {
4000
+ let end = index + 1;
4001
+ while (end < text.length && text[end] !== char) {
4002
+ end += text[end] === "\\" ? 2 : 1;
4003
+ }
4004
+ index = end;
4005
+ } else if ("{[(".includes(char)) {
4006
+ depth += 1;
4007
+ } else if ("}])".includes(char)) {
4008
+ depth -= 1;
4009
+ if (depth === 0) {
4010
+ parts.push(text.slice(start, index));
4011
+ return { parts: parts.filter((part) => part.trim() !== ""), end: index };
4012
+ }
4013
+ } else if (char === "," && depth === 1) {
4014
+ parts.push(text.slice(start, index));
4015
+ start = index + 1;
4016
+ }
4017
+ }
4018
+ return null;
2788
4019
  }
2789
- function sliceByLocation(source, location) {
2790
- const lines = source.split("\n");
2791
- const { start, end } = location;
2792
- const first = lines[start.line - 1];
2793
- const last = lines[end.line - 1];
2794
- if (first === void 0 || last === void 0 || start.column < 1 || end.column < 1 || end.line < start.line || end.line === start.line && end.column < start.column) {
4020
+ function property(part) {
4021
+ const match = new RegExp(`^\\s*(${STRING_LITERAL}|[\\w$]+)\\s*:([\\s\\S]*)$`).exec(part);
4022
+ if (match === null) {
2795
4023
  return null;
2796
4024
  }
2797
- if (start.line === end.line) {
2798
- return first.slice(start.column - 1, end.column - 1);
4025
+ const key = match[1];
4026
+ return { key: stringValue(key) ?? key, value: match[2].trim() };
4027
+ }
4028
+ function aliasPath(expression, file, text, depth = 0) {
4029
+ const value = expression.trim();
4030
+ const here = path7.dirname(file);
4031
+ if (value === "__dirname" || value === "import.meta.dirname" || /^(?:path\.)?dirname\(\s*(?:url\.)?fileURLToPath\(\s*import\.meta\.url\s*\)\s*\)$/.test(value)) {
4032
+ return here;
4033
+ }
4034
+ const url = new RegExp(
4035
+ `^(?:(?:url\\.)?fileURLToPath\\(\\s*new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\s*\\)|new URL\\(\\s*(${STRING_LITERAL})\\s*,\\s*import\\.meta\\.url\\s*\\)\\.pathname)$`
4036
+ ).exec(value);
4037
+ if (url !== null) {
4038
+ return path7.resolve(here, stringValue(url[1] ?? url[2]));
4039
+ }
4040
+ const call = /^(?:path\.)?(?:resolve|join)\s*\(/.exec(value);
4041
+ if (call !== null) {
4042
+ const found = partsOf2(value, call[0].length - 1);
4043
+ if (found === null || value.slice(found.end + 1).trim() !== "" || found.parts.length === 0) {
4044
+ return null;
4045
+ }
4046
+ const [first, ...rest] = found.parts;
4047
+ const base = aliasPath(first, file, text, depth + 1);
4048
+ const parts = rest.map(stringValue);
4049
+ return base === null || parts.some((part) => part === null) ? null : path7.resolve(base, ...parts);
2799
4050
  }
2800
- return [
2801
- first.slice(start.column - 1),
2802
- ...lines.slice(start.line, end.line - 1),
2803
- last.slice(0, end.column - 1)
2804
- ].join("\n");
4051
+ if (/^[A-Za-z_$][\w$]*$/.test(value) && depth < 4) {
4052
+ const declarations = text.match(
4053
+ new RegExp(`\\b(?:const|let|var|function|class)\\s+${value.replace(/\$/g, "\\$")}\\b`, "g")
4054
+ );
4055
+ const bound = new RegExp(`\\bconst\\s+${value.replace(/\$/g, "\\$")}\\s*=\\s*([^;\\n]+)`).exec(
4056
+ text
4057
+ );
4058
+ return declarations?.length === 1 && bound !== null ? aliasPath(bound[1], file, text, depth + 1) : null;
4059
+ }
4060
+ return null;
2805
4061
  }
2806
- function keyOf(mutator, original, replacement) {
2807
- return JSON.stringify([mutator, normalizeCode(original), normalizeCode(replacement)]);
4062
+ var PACKAGE_NAME = /^(?:@[\w-][\w.-]*\/)?[\w-][\w.-]*$/;
4063
+ function isDirectory(file) {
4064
+ try {
4065
+ return fs7.statSync(file).isDirectory();
4066
+ } catch {
4067
+ return false;
4068
+ }
2808
4069
  }
2809
- function describeMutant(file, entry) {
2810
- const { mutant } = entry;
2811
- return {
2812
- file,
2813
- line: mutant.location.start.line,
2814
- column: mutant.location.start.column,
2815
- status: mutant.status,
2816
- mutator: mutant.mutatorName,
2817
- original: normalizeCode(entry.original),
2818
- replacement: normalizeCode(entry.replacement),
2819
- occurrence: entry.occurrence,
2820
- sameKeyCount: entry.sameKeyCount,
2821
- allowance: {
2822
- mutator: mutant.mutatorName,
2823
- original: normalizeCode(entry.original),
2824
- replacement: normalizeCode(entry.replacement),
2825
- ...entry.sameKeyCount > 1 ? { occurrence: entry.occurrence } : {},
2826
- reason: ""
2827
- }
2828
- };
4070
+ function regexPrefix(literal) {
4071
+ const match = /^\/\^((?:[\w@~#%-]|\\[./@~#-])*)\\\/\/$/.exec(literal.trim());
4072
+ return match === null ? null : `${match[1].replace(/\\(.)/g, "$1")}/`;
2829
4073
  }
2830
- function describeAllowance(entry) {
2831
- const occurrence = entry.occurrence === void 0 ? "" : ` / occurrence ${entry.occurrence}`;
2832
- return `${entry.mutator} / ${normalizeCode(entry.original)} \u2192 ${normalizeCode(entry.replacement)}${occurrence}`;
4074
+ function mapperPrefix(pattern) {
4075
+ const match = /^\^((?:[\w@~#%/-]|\\[./@~#-])*)\(\.\*\)\$?$/.exec(pattern);
4076
+ return match === null ? null : match[1].replace(/\\(.)/g, "$1");
2833
4077
  }
2834
- function judgeMutationReport(input) {
2835
- const violations = [];
2836
- const survivors = [];
2837
- const unexpectedTimeouts = [];
2838
- const summary = {
2839
- total: 0,
2840
- detected: 0,
2841
- timeout: 0,
2842
- allowed: 0,
2843
- notEvaluable: 0,
2844
- unallowed: 0,
2845
- expectedTimeout: 0
2846
- };
2847
- const files = /* @__PURE__ */ new Map();
2848
- for (const [file, result] of Object.entries(input.report.files ?? {})) {
2849
- files.set(normalizePath(file), result);
2850
- }
2851
- for (const gate of input.gates) {
2852
- const gatePath = normalizePath(gate.path);
2853
- const shapeViolations = validateEquivalentMutants(gate.path, gate.equivalentMutants);
2854
- violations.push(...shapeViolations);
2855
- violations.push(
2856
- ...validateEquivalentMutants(gate.path, gate.expectedTimeouts, "expectedTimeouts")
2857
- );
2858
- const result = files.get(gatePath);
2859
- if (!result || !Array.isArray(result.mutants) || result.mutants.length === 0) {
2860
- violations.push({
2861
- file: gate.path,
2862
- message: "no mutants in the report (the gate was not mutation tested)"
2863
- });
2864
- continue;
2865
- }
2866
- const diskSource = input.readSource(gate.path);
2867
- const source = result.source ?? diskSource;
2868
- if (source === void 0) {
2869
- violations.push({
2870
- file: gate.path,
2871
- message: "source is neither in the report nor readable on disk"
2872
- });
2873
- continue;
2874
- }
2875
- if (result.source !== void 0 && diskSource !== void 0 && normalizeNewlines(result.source) !== normalizeNewlines(diskSource)) {
2876
- violations.push({
2877
- file: gate.path,
2878
- message: "the report was made from a different version of this file. Run Stryker again"
2879
- });
2880
- continue;
2881
- }
2882
- const located = [];
2883
- const byKey = /* @__PURE__ */ new Map();
2884
- const sorted2 = [...result.mutants].sort(
2885
- (a, b) => a.location.start.line - b.location.start.line || a.location.start.column - b.location.start.column
2886
- );
2887
- let locationsValid = true;
2888
- for (const mutant of sorted2) {
2889
- const original = sliceByLocation(normalizeNewlines(source), mutant.location);
2890
- if (original === null) {
2891
- violations.push({
2892
- file: gate.path,
2893
- line: mutant.location.start.line,
2894
- message: `the report location of a ${mutant.mutatorName} mutant is outside the source`
2895
- });
2896
- locationsValid = false;
2897
- continue;
2898
- }
2899
- const replacement = mutant.replacement ?? "";
2900
- const key = keyOf(mutant.mutatorName, original, replacement);
2901
- const group = byKey.get(key) ?? [];
2902
- const entry = {
2903
- mutant,
2904
- original,
2905
- replacement,
2906
- occurrence: group.length + 1,
2907
- sameKeyCount: 0
2908
- };
2909
- group.push(entry);
2910
- byKey.set(key, group);
2911
- located.push(entry);
2912
- }
2913
- for (const group of byKey.values()) {
2914
- for (const entry of group) {
2915
- entry.sameKeyCount = group.length;
2916
- }
2917
- }
2918
- if (!locationsValid) {
2919
- continue;
4078
+ var ROOT_DIR_HERE = /* @__PURE__ */ new Set([
4079
+ "__dirname",
4080
+ "import.meta.dirname",
4081
+ "import.meta.url",
4082
+ "'.'",
4083
+ '"."',
4084
+ "'./'",
4085
+ '"./"'
4086
+ ]);
4087
+ function unexplainedMaps(input) {
4088
+ const { dir, aliases } = input;
4089
+ const reasons = [];
4090
+ const mapped = [];
4091
+ const shown = (file) => toPosix2(path7.relative(dir, file));
4092
+ const under = (base, rest) => rest === "" ? `${base}/` : path7.join(base, rest);
4093
+ const named = (prefix, target) => {
4094
+ const alias = aliases.find((candidate) => prefix.startsWith(candidate.prefix));
4095
+ if (alias === void 0) {
4096
+ return false;
2920
4097
  }
2921
- const claim = (field, entries, noun, accept) => {
2922
- const claimed = /* @__PURE__ */ new Set();
2923
- if (!Array.isArray(entries)) {
2924
- return claimed;
2925
- }
2926
- entries.forEach((entry, index) => {
2927
- if (!isUsableEquivalentMutant(entry)) {
2928
- return;
2929
- }
2930
- const at = `${gate.path}: ${field}[${index}] (${describeAllowance(entry)})`;
2931
- const group = byKey.get(keyOf(entry.mutator, entry.original, entry.replacement)) ?? [];
2932
- if (entry.occurrence === void 0 && group.length > 1) {
2933
- violations.push({
2934
- file: MANIFEST_FILE,
2935
- message: `${at}: matches ${group.length} mutants in the file. Add "occurrence" (1-based) to pick one`
2936
- });
2937
- return;
4098
+ const full = alias.target + prefix.slice(alias.prefix.length);
4099
+ const slash = full === "" || full.endsWith("/");
4100
+ return path7.resolve(dir, full) === path7.resolve(target) && slash === target.endsWith("/");
4101
+ };
4102
+ const advice = (prefix, target) => {
4103
+ const relative = toPosix2(path7.relative(dir, target));
4104
+ const shownTarget = relative === "" ? "./" : `${relative}/`;
4105
+ const shorter = aliases.find((alias) => prefix.startsWith(alias.prefix));
4106
+ const before = shorter === void 0 ? "" : shorter.prefix === prefix ? `, in place of the alias of ${prefix}` : `, before the alias of ${shorter.prefix}`;
4107
+ return ` (add { "prefix": "${prefix}", "target": "${shownTarget}" } to settings.importAliases${before})`;
4108
+ };
4109
+ const missing = (file, key, word, prefix, target) => `${shown(file)} maps ${key} with ${word} in a way settings.importAliases does not name` + (prefix !== void 0 && target !== void 0 && isDirectory(target) ? advice(prefix, target) : "");
4110
+ for (const { file, text: raw, mapperOnly } of input.runner) {
4111
+ let viteAlias2 = function(part, list) {
4112
+ let key = null;
4113
+ let prefix = null;
4114
+ let to = null;
4115
+ if (list) {
4116
+ const object = /^\s*\{/.test(part) ? partsOf2(part, part.indexOf("{")) : null;
4117
+ const fields = Object.fromEntries(
4118
+ (object?.parts ?? []).map(property).map((one) => [one?.key ?? "", one?.value ?? ""])
4119
+ );
4120
+ if (object === null || Object.keys(fields).sort().join() !== "find,replacement" || part.slice(object.end + 1).trim() !== "") {
4121
+ return unreadableAlias2();
2938
4122
  }
2939
- const target = group[(entry.occurrence ?? 1) - 1];
2940
- const refusal = target ? accept(target.mutant.status) : `stale ${noun} (no such mutant in this run). Remove it`;
2941
- if (!target || refusal !== null) {
2942
- violations.push({ file: MANIFEST_FILE, message: `${at}: ${refusal}` });
2943
- return;
4123
+ const find = fields.find;
4124
+ const replacement = fields.replacement;
4125
+ key = stringValue(find);
4126
+ if (key !== null) {
4127
+ prefix = key.endsWith("/") ? null : `${key}/`;
4128
+ const target = aliasPath(replacement, file, text);
4129
+ to = target === null ? null : `${target}/`;
4130
+ } else {
4131
+ key = find.trim();
4132
+ prefix = regexPrefix(find);
4133
+ const slashed = new RegExp(`^([\\s\\S]+?)\\s*\\+\\s*(?:'/'|"/")$`).exec(replacement);
4134
+ const target = slashed === null ? null : aliasPath(slashed[1], file, text);
4135
+ to = target === null ? null : `${target}/`;
4136
+ if (prefix === null && to === null) {
4137
+ return unreadableAlias2();
4138
+ }
2944
4139
  }
2945
- if (claimed.has(target)) {
2946
- violations.push({
2947
- file: MANIFEST_FILE,
2948
- message: `${at}: another ${noun} already covers this mutant`
2949
- });
2950
- return;
4140
+ } else {
4141
+ const entry = property(part);
4142
+ if (entry === null) {
4143
+ return unreadableAlias2();
2951
4144
  }
2952
- claimed.add(target);
2953
- });
2954
- return claimed;
4145
+ key = entry.key;
4146
+ prefix = key.endsWith("/") ? null : `${key}/`;
4147
+ const target = aliasPath(entry.value, file, text);
4148
+ to = target === null ? null : `${target}/`;
4149
+ }
4150
+ if (prefix === null || to === null || PACKAGE_NAME.test(key) && installed(key, file)) {
4151
+ return missing(file, key, "alias");
4152
+ }
4153
+ if (!named(prefix, to)) {
4154
+ return missing(file, key, "alias", prefix, to);
4155
+ }
4156
+ mapped.push(prefix);
4157
+ return null;
4158
+ }, unreadableAlias2 = function() {
4159
+ return `${shown(file)} names alias in a way that cannot be read as a list of aliases`;
2955
4160
  };
2956
- const allowed = claim(
2957
- "equivalentMutants",
2958
- gate.equivalentMutants,
2959
- "allowance",
2960
- (status) => SURVIVED.has(status) ? null : `stale allowance (the mutant is now ${status}). Remove it`
2961
- );
2962
- const expected = claim(
2963
- "expectedTimeouts",
2964
- gate.expectedTimeouts,
2965
- "entry",
2966
- (status) => SURVIVED.has(status) ? `the mutant ${status === "NoCoverage" ? "has no coverage" : "survived"} instead of timing out. Detect it with a test` : null
2967
- );
2968
- for (const entry of located) {
2969
- const { mutant } = entry;
2970
- summary.total += 1;
2971
- if (DETECTED.has(mutant.status)) {
2972
- summary.detected += 1;
2973
- if (mutant.status === "Timeout") {
2974
- summary.timeout += 1;
2975
- if (expected.has(entry)) {
2976
- summary.expectedTimeout += 1;
2977
- } else {
2978
- unexpectedTimeouts.push(describeMutant(gate.path, entry));
4161
+ var viteAlias = viteAlias2, unreadableAlias = unreadableAlias2;
4162
+ const text = withoutComments(raw);
4163
+ const words = mapperOnly === true ? /\bmoduleNameMapper\b/g : /\b(?:alias|moduleNameMapper)\b/g;
4164
+ for (const match of text.matchAll(words)) {
4165
+ const word = match[0];
4166
+ const opening = /^['"]?\s*:\s*/.exec(text.slice(match.index + word.length));
4167
+ const at = opening === null ? -1 : match.index + word.length + opening[0].length;
4168
+ const found = at === -1 ? null : partsOf2(text, at);
4169
+ const unreadable = `${shown(file)} names ${word} in a way that cannot be read as a list of aliases`;
4170
+ if (found === null || text[at] !== "{" && !(word === "alias" && text[at] === "[")) {
4171
+ reasons.push(unreadable);
4172
+ continue;
4173
+ }
4174
+ for (const part of found.parts) {
4175
+ if (word === "alias") {
4176
+ const reason = viteAlias2(part, text[at] === "[");
4177
+ if (reason !== null) {
4178
+ reasons.push(reason);
2979
4179
  }
4180
+ continue;
2980
4181
  }
2981
- } else if (NOT_EVALUABLE.has(mutant.status)) {
2982
- summary.notEvaluable += 1;
2983
- } else if (SURVIVED.has(mutant.status)) {
2984
- if (allowed.has(entry)) {
2985
- summary.allowed += 1;
4182
+ const entry = property(part);
4183
+ if (entry === null) {
4184
+ reasons.push(unreadable);
2986
4185
  continue;
2987
4186
  }
2988
- summary.unallowed += 1;
2989
- survivors.push(describeMutant(gate.path, entry));
2990
- } else {
2991
- violations.push({
2992
- file: gate.path,
2993
- line: mutant.location.start.line,
2994
- message: mutant.status === "Ignored" ? `a ${mutant.mutatorName} mutant was ignored. Taking mutants out with Stryker comments or mutator exclusions is not allowed` : `a ${mutant.mutatorName} mutant was not evaluated (status: ${mutant.status})`
2995
- });
4187
+ const value = stringValue(entry.value);
4188
+ const root = [...text.matchAll(/\brootDir\b['"]?\s*:\s*([^,}\n]+)/g)].map((one) => one[1].trim()).find((set) => !ROOT_DIR_HERE.has(set));
4189
+ if (root !== void 0 && (value === null || value.includes("<rootDir>"))) {
4190
+ reasons.push(`${shown(file)} sets rootDir to a directory that cannot be told: ${root}`);
4191
+ continue;
4192
+ }
4193
+ if (value !== null && !value.includes("$")) {
4194
+ continue;
4195
+ }
4196
+ const prefix = mapperPrefix(entry.key);
4197
+ const target = value === null ? null : /^<rootDir>\/(.*)\$1$/.exec(value);
4198
+ const to = target === null ? null : under(path7.dirname(file), target[1]);
4199
+ if (prefix === null || to === null) {
4200
+ reasons.push(missing(file, entry.key, word));
4201
+ } else if (!named(prefix, to)) {
4202
+ reasons.push(missing(file, entry.key, word, prefix, to));
4203
+ } else {
4204
+ mapped.push(prefix);
4205
+ }
2996
4206
  }
2997
4207
  }
2998
4208
  }
2999
- if (summary.total === 0) {
3000
- violations.push({
3001
- file: MANIFEST_FILE,
3002
- message: "no mutants were evaluated (the mutation test ran on nothing)"
3003
- });
3004
- }
3005
- const maxTimeouts = input.maxTimeouts ?? 0;
3006
- if (unexpectedTimeouts.length > maxTimeouts) {
3007
- violations.push({
3008
- file: MANIFEST_FILE,
3009
- message: `${unexpectedTimeouts.length} mutant(s) timed out outside expectedTimeouts; settings.mutation.maxTimeouts allows ${maxTimeouts}`
3010
- });
4209
+ const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(path7.basename(file)));
4210
+ const withBase = tsconfigs.filter(({ text }) => text !== null && /"baseUrl"\s*:/.test(text));
4211
+ for (const { file, text } of input.compiler) {
4212
+ if (text === null) {
4213
+ continue;
4214
+ }
4215
+ const plain2 = withoutComments(text);
4216
+ const name = path7.basename(file);
4217
+ const swc = name === ".swcrc";
4218
+ if (!/^[jt]sconfig/.test(name) && !swc) {
4219
+ const babel = /"babel"\s*:\s*/.exec(plain2);
4220
+ const own2 = name !== "package.json" ? plain2 : babel === null ? "" : (() => {
4221
+ const found2 = partsOf2(plain2, babel.index + babel[0].length);
4222
+ return found2 === null ? plain2 : found2.parts.join(",");
4223
+ })();
4224
+ const maps = /module-resolver|\balias\b/.exec(own2);
4225
+ if (maps !== null) {
4226
+ reasons.push(`${shown(file)} maps modules (${maps[0]}), which cannot be told`);
4227
+ }
4228
+ continue;
4229
+ }
4230
+ const paths = /"paths"\s*:\s*/.exec(plain2);
4231
+ if (paths === null || !swc && input.testRunner === "jest") {
4232
+ continue;
4233
+ }
4234
+ const at = paths.index + paths[0].length;
4235
+ const found = plain2[at] === "{" ? partsOf2(plain2, at) : null;
4236
+ if (found === null) {
4237
+ reasons.push(`${shown(file)} names paths in a way that cannot be read as a list of aliases`);
4238
+ continue;
4239
+ }
4240
+ const own = /"baseUrl"\s*:\s*"([^"]*)"/.exec(plain2);
4241
+ if (own === null && !swc && withBase.length > 0) {
4242
+ reasons.push(`${shown(file)} names paths, and baseUrl is set in another file`);
4243
+ continue;
4244
+ }
4245
+ const base = path7.resolve(path7.dirname(file), own?.[1] ?? ".");
4246
+ for (const part of found.parts) {
4247
+ const entry = property(part);
4248
+ if (entry === null) {
4249
+ reasons.push(
4250
+ `${shown(file)} names paths in a way that cannot be read as a list of aliases`
4251
+ );
4252
+ continue;
4253
+ }
4254
+ const list = entry.value.startsWith("[") ? partsOf2(entry.value, 0) : null;
4255
+ const target = list?.parts.length === 1 ? stringValue(list.parts[0]) : null;
4256
+ const prefix = entry.key.endsWith("*") ? entry.key.slice(0, -1) : null;
4257
+ if (prefix === null && target !== null && !target.includes("*") && path7.resolve(base, target).split(path7.sep).includes("node_modules")) {
4258
+ continue;
4259
+ }
4260
+ const directory = target?.endsWith("*") ? target.slice(0, -1) : null;
4261
+ if (!swc && prefix !== null && mapped.some((one) => prefix.startsWith(one))) {
4262
+ continue;
4263
+ }
4264
+ if (prefix === null || directory === null) {
4265
+ reasons.push(missing(file, entry.key, "paths"));
4266
+ } else if (!named(prefix, under(base, directory))) {
4267
+ reasons.push(missing(file, entry.key, "paths", prefix, under(base, directory)));
4268
+ }
4269
+ }
3011
4270
  }
3012
- return { violations, survivors, unexpectedTimeouts, summary };
4271
+ return reasons.length === 0 ? null : [...new Set(reasons)].join("; ");
3013
4272
  }
3014
- function formatSurvivor(survivor) {
3015
- const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
3016
- return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
4273
+
4274
+ // src/commands/mutation.ts
4275
+ import { spawnSync as spawnSync4 } from "child_process";
4276
+ import fs10 from "fs";
4277
+ import path10 from "path";
4278
+ import { pathToFileURL } from "url";
4279
+
4280
+ // src/main-worktree.ts
4281
+ import { spawnSync as spawnSync3 } from "child_process";
4282
+ import fs8 from "fs";
4283
+ import path8 from "path";
4284
+ function git2(dir, args) {
4285
+ const result = spawnSync3("git", args, {
4286
+ cwd: dir,
4287
+ env: gitEnvironment(process.env),
4288
+ encoding: "utf8"
4289
+ });
4290
+ return result.error || result.status !== 0 ? null : result.stdout.trim().split("\n");
4291
+ }
4292
+ function linkedWorktreeOf(dir) {
4293
+ const found = git2(dir, [
4294
+ "rev-parse",
4295
+ "--path-format=absolute",
4296
+ "--show-toplevel",
4297
+ "--git-dir",
4298
+ "--git-common-dir"
4299
+ ]);
4300
+ if (found === null || found.length !== 3) {
4301
+ return null;
4302
+ }
4303
+ const [top, gitDir, common] = found;
4304
+ if (gitDir === common || path8.basename(common) !== ".git") {
4305
+ return null;
4306
+ }
4307
+ try {
4308
+ const main2 = fs8.realpathSync(path8.dirname(common));
4309
+ const real = fs8.realpathSync(top);
4310
+ return main2 === real ? null : { top: real, main: main2 };
4311
+ } catch {
4312
+ return null;
4313
+ }
3017
4314
  }
3018
- function formatSummary(summary) {
3019
- return `mutants ${summary.total} / detected ${summary.detected} (timeout ${summary.timeout}) / allowed equivalent ${summary.allowed} / not evaluable ${summary.notEvaluable} / unallowed survivors ${summary.unallowed}`;
4315
+ function inMainWorktree(linked, file) {
4316
+ const relative = path8.relative(linked.top, file);
4317
+ if (relative === "" || relative.startsWith("..") || path8.isAbsolute(relative)) {
4318
+ return null;
4319
+ }
4320
+ return path8.join(linked.main, relative);
3020
4321
  }
3021
4322
 
3022
4323
  // src/commands/mutation-result.ts
4324
+ import fs9 from "fs";
4325
+ import path9 from "path";
3023
4326
  function reportPathOf(manifest, override) {
3024
- return path7.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
4327
+ return path9.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
3025
4328
  }
3026
- function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0) {
4329
+ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0, retried = null) {
3027
4330
  const manifest = readManifest(dir);
3028
4331
  const gates = only.length === 0 ? manifest.gates : manifest.gates.filter((gate) => only.includes(gate.path));
3029
4332
  const caveat = only.length === 0 ? "" : gateRunCaveat(gates.length, manifest.gates.length);
3030
4333
  const reportPath = reportPathOf(manifest, reportOverride);
3031
- const shown = path7.relative(manifest.dir, reportPath) || reportPath;
4334
+ const shown = path9.relative(manifest.dir, reportPath) || reportPath;
3032
4335
  let report;
3033
4336
  try {
3034
- report = JSON.parse(fs7.readFileSync(reportPath, "utf8"));
4337
+ report = JSON.parse(fs9.readFileSync(reportPath, "utf8"));
3035
4338
  } catch (error) {
3036
4339
  io2.err(
3037
4340
  `test-gates mutation: cannot read the Stryker JSON report ${shown} (${error.message}). Did Stryker finish, with the "json" reporter writing to this path?`
@@ -3048,7 +4351,7 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3048
4351
  maxTimeouts: manifest.settings.maxTimeouts,
3049
4352
  readSource: (gatePath) => {
3050
4353
  try {
3051
- return fs7.readFileSync(path7.join(manifest.dir, gatePath), "utf8");
4354
+ return fs9.readFileSync(path9.join(manifest.dir, gatePath), "utf8");
3052
4355
  } catch {
3053
4356
  return void 0;
3054
4357
  }
@@ -3096,8 +4399,34 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3096
4399
  io2.err(` - ${formatSurvivor(mutant)}`);
3097
4400
  }
3098
4401
  io2.err("");
3099
- io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
3100
- io2.err("can time out instead. Run again with less load (-- --concurrency 1). Only for a");
4402
+ if (retried === null || retried === "failed") {
4403
+ io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
4404
+ if (retried === null) {
4405
+ io2.err(
4406
+ "can time out instead. `test-gates mutation` runs such timeouts again one at a time"
4407
+ );
4408
+ io2.err("(--concurrency 1) before it fails; these were not run again. Only for a");
4409
+ } else {
4410
+ io2.err(
4411
+ "can time out instead. They were run again one at a time (--concurrency 1), but that"
4412
+ );
4413
+ io2.err("run gave no result (see above), so these are those of the first run. Only for a");
4414
+ }
4415
+ } else if (retried.missing === 0) {
4416
+ io2.err("They timed out again when they were run one at a time (--concurrency 1), so they");
4417
+ io2.err("most likely never finish. Look at each mutant first. Only for a");
4418
+ } else {
4419
+ const again = retried.statuses["Timeout"] ?? 0;
4420
+ if (again > 0) {
4421
+ io2.err(
4422
+ `${again} of them timed out again when they were run one at a time (--concurrency 1).`
4423
+ );
4424
+ }
4425
+ io2.err(
4426
+ `${retried.missing} of them were not in the report of the run again (--concurrency 1),`
4427
+ );
4428
+ io2.err("so they keep the timeout of the first run. Look at each mutant first. Only for a");
4429
+ }
3101
4430
  io2.err("mutant that can never finish (a loop that no longer ends), add it to");
3102
4431
  io2.err('"expectedTimeouts" of the gate in test-gates.json and fill in "reason"');
3103
4432
  io2.err("(settings.mutation.maxTimeouts raises the limit for the whole project):");
@@ -3116,16 +4445,16 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3116
4445
  // src/commands/mutation.ts
3117
4446
  var CONFIG_MARK = "\0test-gates-stryker-config\0";
3118
4447
  function loadStrykerConfig(manifest, config) {
3119
- const file = path8.resolve(manifest.dir, config);
4448
+ const file = path10.resolve(manifest.dir, config);
3120
4449
  if (file.endsWith(".json")) {
3121
4450
  try {
3122
- return JSON.parse(fs8.readFileSync(file, "utf8"));
4451
+ return JSON.parse(fs10.readFileSync(file, "utf8"));
3123
4452
  } catch {
3124
4453
  return void 0;
3125
4454
  }
3126
4455
  }
3127
- const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile,}) + ${JSON.stringify(CONFIG_MARK)});`;
3128
- const result = spawnSync2(process.execPath, ["--input-type=module", "--eval", script], {
4456
+ const script = `const loaded = await import(${JSON.stringify(pathToFileURL(file).href)});const config = loaded.default?.default ?? loaded.default ?? loaded;process.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify({incremental: config.incremental, force: config.force,incrementalFile: config.incrementalFile, tempDirName: config.tempDirName,testRunner: config.testRunner, vitestConfig: config.vitest?.configFile,jestConfig: config.jest?.configFile, checkers: config.checkers,htmlReporter: config.htmlReporter, jsonReporter: config.jsonReporter,ignorePatterns: config.ignorePatterns, cleanTempDir: config.cleanTempDir,}) + ${JSON.stringify(CONFIG_MARK)});`;
4457
+ const result = spawnSync4(process.execPath, ["--input-type=module", "--eval", script], {
3129
4458
  cwd: manifest.dir,
3130
4459
  encoding: "utf8",
3131
4460
  timeout: 6e4
@@ -3138,7 +4467,7 @@ function loadStrykerConfig(manifest, config) {
3138
4467
  }
3139
4468
  function modifiedAt(file) {
3140
4469
  try {
3141
- const stat = fs8.statSync(file, { bigint: true });
4470
+ const stat = fs10.statSync(file, { bigint: true });
3142
4471
  return `${stat.mtimeNs} ${stat.ino}`;
3143
4472
  } catch {
3144
4473
  return null;
@@ -3147,6 +4476,7 @@ function modifiedAt(file) {
3147
4476
  function guardRecordPath(incrementalFile) {
3148
4477
  return `${incrementalFile}.test-gates.json`;
3149
4478
  }
4479
+ var forcedLine = (reason) => `test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${reason}`;
3150
4480
  function coveringSpecs(report, gates) {
3151
4481
  const fileOf = /* @__PURE__ */ new Map();
3152
4482
  for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
@@ -3173,11 +4503,27 @@ function coveringSpecs(report, gates) {
3173
4503
  }
3174
4504
  return covering;
3175
4505
  }
3176
- function decideReuse(manifest, config, strykerArgs, part, affected) {
4506
+ function readSeed(incrementalFile, recordFile) {
4507
+ try {
4508
+ const incremental = fs10.readFileSync(incrementalFile);
4509
+ const record = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
4510
+ const named = record?.incremental;
4511
+ return named === hashOf(incremental) ? { incremental, record } : null;
4512
+ } catch {
4513
+ return null;
4514
+ }
4515
+ }
4516
+ function decideReuse(manifest, config, strykerArgs, part, affected, found = () => void 0) {
3177
4517
  const loaded = loadStrykerConfig(manifest, config);
3178
4518
  const options = incrementalOptions(loaded, strykerArgs);
3179
- const base = fs8.realpathSync(manifest.dir);
3180
- const configs = () => runnerConfigs(base, config, loaded ?? {});
4519
+ const base = fs10.realpathSync(manifest.dir);
4520
+ const checkersArg = strykerArgs.some(
4521
+ (arg) => arg === "--checkers" || arg.startsWith("--checkers=")
4522
+ );
4523
+ const configs = () => runnerConfigs(base, config, {
4524
+ ...loaded ?? {},
4525
+ ...checkersArg ? { checkers: ["--checkers"] } : {}
4526
+ });
3181
4527
  const decision = {
3182
4528
  incremental: false,
3183
4529
  incrementalFile: null,
@@ -3194,25 +4540,44 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3194
4540
  last: void 0,
3195
4541
  ci: affected ? affectedRefused(process.env) : null,
3196
4542
  unchanged: null,
3197
- configs
4543
+ configs,
4544
+ temp: {
4545
+ dir: loaded === void 0 ? null : path10.resolve(base, options.tempDirName),
4546
+ keeps: loaded?.cleanTempDir === false
4547
+ },
4548
+ mainWorktree: null,
4549
+ seed: null
3198
4550
  };
4551
+ found(decision.temp);
3199
4552
  if (loaded !== void 0 && !options.incremental) {
3200
4553
  return decision;
3201
4554
  }
3202
4555
  decision.incremental = true;
3203
- const incrementalFile = path8.resolve(base, options.incrementalFile);
4556
+ const incrementalFile = path10.resolve(base, options.incrementalFile);
3204
4557
  const recordFile = guardRecordPath(incrementalFile);
3205
4558
  decision.incrementalFile = incrementalFile;
3206
4559
  decision.recordFile = recordFile;
3207
4560
  let snapshot = null;
3208
4561
  let root = base;
3209
4562
  let failure = "the Stryker config could not be loaded to see where the incremental file is";
4563
+ const reports = reportFiles(loaded).map((file) => path10.resolve(base, file));
4564
+ const ignores = strykerIgnores(loaded);
4565
+ const read2 = (file) => {
4566
+ const relative = path10.relative(base, file);
4567
+ const inside = !relative.startsWith("..") && !path10.isAbsolute(relative);
4568
+ return !reports.includes(file) && !(inside && ignores(relative.split(path10.sep).join("/")));
4569
+ };
3210
4570
  if (loaded !== void 0) {
3211
4571
  const scope = (listed2) => {
3212
4572
  try {
3213
- const found = loadedScope(manifest, configs(), listed2);
3214
- decision.scope = "files" in found ? { files: found.files.size } : found;
3215
- return "files" in found ? found.files : null;
4573
+ const found2 = loadedScope(manifest, configs(), listed2);
4574
+ if (!("files" in found2)) {
4575
+ decision.scope = found2;
4576
+ return null;
4577
+ }
4578
+ const files = new Set([...found2.files].filter(read2));
4579
+ decision.scope = { files: files.size };
4580
+ return files;
3216
4581
  } catch (error) {
3217
4582
  decision.scope = { every: error.message };
3218
4583
  return null;
@@ -3226,8 +4591,9 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3226
4591
  written: [
3227
4592
  incrementalFile,
3228
4593
  recordFile,
3229
- path8.resolve(base, options.tempDirName),
3230
- path8.resolve(base, manifest.settings.strykerReportFile)
4594
+ path10.resolve(base, options.tempDirName),
4595
+ path10.resolve(base, manifest.settings.strykerReportFile),
4596
+ ...reports
3231
4597
  ],
3232
4598
  args: comparableArgs(strykerArgs)
3233
4599
  }));
@@ -3239,17 +4605,32 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3239
4605
  decision.snapshot = snapshot;
3240
4606
  decision.failure = snapshot === null ? failure : null;
3241
4607
  decision.root = root;
3242
- const incremental = hashFile(incrementalFile);
4608
+ const tempDir = path10.basename(options.tempDirName);
4609
+ if (!part && !options.force && snapshot?.mode === "git" && !path10.relative(root, base).split(path10.sep).includes(tempDir) && !fs10.existsSync(incrementalFile) && !fs10.existsSync(recordFile)) {
4610
+ const linked = linkedWorktreeOf(base);
4611
+ const mainIncremental = linked === null ? null : inMainWorktree(linked, incrementalFile);
4612
+ const mainRecord = linked === null ? null : inMainWorktree(linked, recordFile);
4613
+ if (linked !== null && mainIncremental !== null && mainRecord !== null) {
4614
+ decision.mainWorktree = linked.main;
4615
+ const seed = readSeed(mainIncremental, mainRecord);
4616
+ if (seed !== null) {
4617
+ decision.seed = seed;
4618
+ }
4619
+ }
4620
+ }
4621
+ const incremental = decision.seed === null ? hashFile(incrementalFile) : hashOf(decision.seed.incremental);
3243
4622
  decision.incrementalHash = incremental;
3244
4623
  if (part) {
3245
4624
  decision.force = incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null;
3246
4625
  return decision;
3247
4626
  }
3248
- let stored;
3249
- try {
3250
- stored = JSON.parse(fs8.readFileSync(recordFile, "utf8"));
3251
- } catch {
3252
- stored = void 0;
4627
+ let stored = decision.seed?.record;
4628
+ if (decision.seed === null) {
4629
+ try {
4630
+ stored = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
4631
+ } catch {
4632
+ stored = void 0;
4633
+ }
3253
4634
  }
3254
4635
  const ci = decision.ci;
3255
4636
  let force = null;
@@ -3260,14 +4641,21 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3260
4641
  force = failure;
3261
4642
  } else if (!affected) {
3262
4643
  force = refuseReuse({ record: stored, incremental, snapshot });
4644
+ if (force === null) {
4645
+ const read3 = readRecord({ record: stored, incremental, snapshot });
4646
+ last = "record" in read3 ? read3.record.affected : void 0;
4647
+ if (decision.seed !== null) {
4648
+ force = blindInMainRecord(manifest, { root, snapshot, configs, last });
4649
+ }
4650
+ }
3263
4651
  } else if (ci !== null) {
3264
4652
  force = ci;
3265
4653
  } else {
3266
- const read2 = readRecord({ record: stored, incremental, snapshot });
3267
- if ("refused" in read2) {
3268
- force = read2.refused;
4654
+ const read3 = readRecord({ record: stored, incremental, snapshot });
4655
+ if ("refused" in read3) {
4656
+ force = read3.refused;
3269
4657
  } else {
3270
- last = read2.record.affected;
4658
+ last = read3.record.affected;
3271
4659
  try {
3272
4660
  const inputs = affectedInputs(manifest, {
3273
4661
  root,
@@ -3279,14 +4667,16 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3279
4667
  )
3280
4668
  });
3281
4669
  plan = planAffected({
3282
- before: { files: read2.record.snapshot.files, affected: last },
4670
+ before: { files: read3.record.snapshot.files, affected: last },
3283
4671
  now: snapshot.files,
3284
4672
  subproject: inputs.subproject,
3285
4673
  config: inputs.config,
3286
4674
  gates: manifest.gates.map((gate) => ({
3287
4675
  path: gate.path,
3288
- files: inputs.gates[gate.path]?.files ?? []
3289
- }))
4676
+ files: inputs.gates[gate.path]?.files ?? [],
4677
+ blind: inputs.gates[gate.path]?.blind
4678
+ })),
4679
+ seeded: decision.seed !== null
3290
4680
  });
3291
4681
  } catch (error) {
3292
4682
  if (!(error instanceof AffectedError)) {
@@ -3295,7 +4685,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3295
4685
  plan = { full: `what a gate depends on cannot be told: ${error.message}` };
3296
4686
  }
3297
4687
  if ("full" in plan) {
3298
- if (refuseReuse({ record: stored, incremental, snapshot }) === null) {
4688
+ if (decision.seed === null && refuseReuse({ record: stored, incremental, snapshot }) === null) {
3299
4689
  decision.unchanged = plan.full;
3300
4690
  plan = null;
3301
4691
  } else {
@@ -3312,9 +4702,45 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
3312
4702
  decision.last = last;
3313
4703
  return decision;
3314
4704
  }
3315
- function guardIncremental(manifest, config, strykerArgs, part, affected) {
3316
- const nothing = { force: null, mutate: null, said: [], record: () => void 0 };
3317
- const decision = decideReuse(manifest, config, strykerArgs, part, affected);
4705
+ function blindInMainRecord(manifest, input) {
4706
+ const { last } = input;
4707
+ const unknown = last === void 0 ? "each gate" : manifest.gates.find((gate) => !Object.hasOwn(last.gates, gate.path))?.path;
4708
+ if (last === void 0 || unknown !== void 0) {
4709
+ return `the record of the main worktree does not say what ${unknown} depends on, so it may read files git does not list, which differ between worktrees`;
4710
+ }
4711
+ try {
4712
+ const inputs = affectedInputs(manifest, {
4713
+ root: input.root,
4714
+ files: input.snapshot.files,
4715
+ configs: input.configs(),
4716
+ tests: last.tests,
4717
+ covering: Object.fromEntries(
4718
+ Object.entries(last.gates).map(([gate, entry]) => [gate, entry.specs])
4719
+ )
4720
+ });
4721
+ for (const gate of manifest.gates) {
4722
+ const blind = inputs.gates[gate.path]?.blind ?? last.gates[gate.path]?.blind;
4723
+ if (blind !== void 0) {
4724
+ return `${gate.path} may read files git does not list, which differ between worktrees (${blind})`;
4725
+ }
4726
+ }
4727
+ return null;
4728
+ } catch (error) {
4729
+ if (!(error instanceof AffectedError)) {
4730
+ throw error;
4731
+ }
4732
+ return `what a gate depends on cannot be told, so it may read files git does not list, which differ between worktrees: ${error.message}`;
4733
+ }
4734
+ }
4735
+ function guardIncremental(manifest, config, strykerArgs, part, affected, found) {
4736
+ const decision = decideReuse(manifest, config, strykerArgs, part, affected, found);
4737
+ const nothing = {
4738
+ force: null,
4739
+ mutate: null,
4740
+ said: [],
4741
+ temp: decision.temp,
4742
+ record: () => void 0
4743
+ };
3318
4744
  if (!decision.incremental) {
3319
4745
  return affected ? {
3320
4746
  ...nothing,
@@ -3323,71 +4749,96 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3323
4749
  ]
3324
4750
  } : nothing;
3325
4751
  }
3326
- const { snapshot, root, plan, last, ci, configs, force } = decision;
4752
+ const { snapshot, root, plan, last, configs, force } = decision;
3327
4753
  const incrementalFile = decision.incrementalFile;
3328
4754
  const recordFile = decision.recordFile;
3329
4755
  const incremental = decision.incrementalHash;
3330
4756
  const options = { force: decision.forced };
3331
4757
  if (part) {
3332
- fs8.rmSync(recordFile, { force: true });
4758
+ fs10.rmSync(recordFile, { force: true });
3333
4759
  return { ...nothing, force };
3334
4760
  }
3335
- fs8.rmSync(recordFile, { force: true });
4761
+ if (decision.seed !== null) {
4762
+ fs10.mkdirSync(path10.dirname(incrementalFile), { recursive: true });
4763
+ fs10.writeFileSync(incrementalFile, decision.seed.incremental);
4764
+ }
4765
+ fs10.rmSync(recordFile, { force: true });
3336
4766
  const before = modifiedAt(incrementalFile);
3337
4767
  const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
3338
4768
  const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
3339
4769
  let ranges = [];
4770
+ let timedOut = 0;
4771
+ let expected = 0;
3340
4772
  if (everyGate === null && incremental !== null && !options.force) {
3341
- const kept = rerun === null ? manifest.gates.map((gate) => gate.path) : manifest.gates.map((gate) => gate.path).filter((gate) => !rerun.includes(gate));
4773
+ const kept = rerun === null ? manifest.gates : manifest.gates.filter((gate) => !rerun.includes(gate.path));
3342
4774
  try {
3343
- ranges = timeoutRanges(
3344
- JSON.parse(fs8.readFileSync(incrementalFile, "utf8")),
4775
+ ({
4776
+ ranges,
4777
+ rerun: timedOut,
4778
+ expected
4779
+ } = timeoutRanges(
4780
+ JSON.parse(fs10.readFileSync(incrementalFile, "utf8")),
3345
4781
  kept
3346
- );
4782
+ ));
3347
4783
  } catch {
3348
- ranges = [];
3349
4784
  }
3350
4785
  }
3351
4786
  const subset = rerun !== null && rerun.length > 0 && everyGate === null ? rerun : [];
4787
+ const ran = affected ? rerun : everyGate !== null || options.force || incremental === null ? null : [];
3352
4788
  let said = [];
3353
4789
  if (affected && !options.force) {
3354
4790
  if (force !== null) {
3355
4791
  said = describePlan({ full: force });
3356
4792
  } else if (plan !== null) {
3357
- said = describePlan(plan, 5, ranges.length);
4793
+ said = describePlan(plan, 5, timedOut, expected);
3358
4794
  } else if (decision.unchanged !== null) {
3359
4795
  said = [
3360
4796
  `test-gates mutation --affected: nothing changed since the last run, every result is reused (gate by gate, ${decision.unchanged.replace(/^what a gate/, "what each gate")})`,
3361
- ...ranges.length > 0 ? [` and ${ranges.length} mutant(s) that timed out in the last run are run again`] : []
4797
+ ...ranges.length > 0 ? [` and ${timedOut} mutant(s) that timed out in the last run are run again`] : [],
4798
+ ...expected > 0 ? [` ${describeExpected(expected)}`] : []
3362
4799
  ];
3363
4800
  }
3364
- } else if (ranges.length > 0) {
3365
- said = [
3366
- `test-gates mutation: ${ranges.length} mutant(s) that timed out in the last run are run again, the other results are reused`
3367
- ];
4801
+ } else {
4802
+ if (ranges.length > 0) {
4803
+ said.push(
4804
+ `test-gates mutation: ${timedOut} mutant(s) that timed out in the last run are run again, the other results are reused`
4805
+ );
4806
+ }
4807
+ if (expected > 0) {
4808
+ said.push(`test-gates mutation: ${describeExpected(expected)}`);
4809
+ }
4810
+ }
4811
+ if (decision.seed !== null) {
4812
+ if (said.length === 0 && everyGate !== null) {
4813
+ said.push(forcedLine(everyGate));
4814
+ }
4815
+ said.unshift(
4816
+ `test-gates mutation: this worktree has no record of its own, so it starts from the one of the main worktree (${decision.mainWorktree})`
4817
+ );
3368
4818
  }
3369
4819
  return {
3370
4820
  // Every gate is run again: the same as a run of every mutant.
3371
4821
  force: everyGate,
3372
4822
  mutate: subset.length + ranges.length > 0 ? [...subset, ...ranges] : null,
3373
4823
  said,
4824
+ temp: decision.temp,
3374
4825
  record: (judged) => {
3375
4826
  const written = hashFile(incrementalFile);
3376
4827
  if (snapshot === null || written === null || modifiedAt(incrementalFile) === before) {
3377
4828
  return;
3378
4829
  }
3379
4830
  const record = { format: RECORD_FORMAT, incremental: written, snapshot };
3380
- if (affected && ci === null && judged !== null) {
3381
- record.affected = affectedRecord(judged, rerun, last);
4831
+ if (affectedRefused(process.env) === null && judged !== null) {
4832
+ record.affected = affectedRecord(judged, ran, last);
3382
4833
  }
3383
4834
  try {
3384
- fs8.writeFileSync(recordFile, `${JSON.stringify(record)}
4835
+ fs10.writeFileSync(recordFile, `${JSON.stringify(record)}
3385
4836
  `);
3386
4837
  } catch {
3387
4838
  }
3388
4839
  }
3389
4840
  };
3390
- function affectedRecord(judged, ran, previous) {
4841
+ function affectedRecord(judged, ran2, previous) {
3391
4842
  const gates = manifest.gates.map((gate) => gate.path);
3392
4843
  const dirty = new Set(
3393
4844
  [...judged.verdict.survivors, ...judged.verdict.unexpectedTimeouts].map((one) => one.file)
@@ -3395,7 +4846,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3395
4846
  const fresh = coveringSpecs(judged.report, gates);
3396
4847
  const covering = /* @__PURE__ */ Object.create(null);
3397
4848
  for (const gate of gates) {
3398
- const specs = ran === null || ran.includes(gate) ? fresh[gate] : previous?.gates[gate]?.specs ?? null;
4849
+ const specs = ran2 === null || ran2.includes(gate) ? fresh[gate] : previous?.gates[gate]?.specs ?? null;
3399
4850
  if (specs !== null && specs !== void 0 && !dirty.has(gate)) {
3400
4851
  covering[gate] = specs;
3401
4852
  }
@@ -3412,12 +4863,16 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
3412
4863
  for (const gate of Object.keys(covering)) {
3413
4864
  const entry = inputs.gates[gate];
3414
4865
  if (entry !== void 0) {
3415
- recorded[gate] = { specs: entry.specs, files: entry.files };
4866
+ recorded[gate] = {
4867
+ specs: entry.specs,
4868
+ files: entry.files,
4869
+ ...entry.blind === void 0 ? {} : { blind: entry.blind }
4870
+ };
3416
4871
  }
3417
4872
  }
3418
4873
  return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
3419
4874
  } catch (error) {
3420
- if (error instanceof AffectedError) {
4875
+ if (error instanceof AffectedError || !affected) {
3421
4876
  return void 0;
3422
4877
  }
3423
4878
  throw error;
@@ -3463,53 +4918,120 @@ function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
3463
4918
  );
3464
4919
  }
3465
4920
  const reportFile = reportPathOf(manifest, null);
3466
- fs8.rmSync(reportFile, { force: true });
3467
- const guard = guardIncremental(manifest, config, strykerArgs, part, affected);
4921
+ fs10.rmSync(reportFile, { force: true });
4922
+ let removed = [];
4923
+ const guard = guardIncremental(manifest, config, strykerArgs, part, affected, (temp) => {
4924
+ removed = removeStaleSandboxes(manifest.dir, temp);
4925
+ });
3468
4926
  if (guard.said.length > 0) {
3469
4927
  guard.said.forEach((line) => io2.out(line));
3470
4928
  } else if (guard.force !== null) {
3471
- io2.out(
3472
- `test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${guard.force}`
3473
- );
4929
+ io2.out(forcedLine(guard.force));
3474
4930
  }
4931
+ removed.forEach((line) => io2.out(line));
3475
4932
  const guardArgs = guard.force === null && guard.mutate === null ? [] : ["--force"];
3476
4933
  const partArgs = part ? ["--mutate", chosen.selected.join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
3477
- const result = spawnSync2(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
3478
- cwd: manifest.dir,
3479
- stdio: "inherit",
3480
- shell: process.platform === "win32"
3481
- });
3482
- if (result.error) {
3483
- io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
3484
- return 1;
3485
- }
3486
- if (result.status !== 0) {
3487
- io2.err(
3488
- `test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
4934
+ const unmark = markRun(manifest.dir, guard.temp);
4935
+ let retried;
4936
+ try {
4937
+ const result = spawnSync4(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
4938
+ cwd: manifest.dir,
4939
+ stdio: "inherit",
4940
+ shell: process.platform === "win32"
4941
+ });
4942
+ if (result.error) {
4943
+ io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
4944
+ return 1;
4945
+ }
4946
+ if (result.status !== 0) {
4947
+ io2.err(
4948
+ `test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
4949
+ );
4950
+ return 1;
4951
+ }
4952
+ retried = retryTimeouts(
4953
+ stryker,
4954
+ [config, ...strykerArgs],
4955
+ manifest,
4956
+ reportFile,
4957
+ part ? manifest.gates.filter((gate) => chosen.selected.includes(gate.path)) : manifest.gates,
4958
+ io2
3489
4959
  );
3490
- return 1;
4960
+ } finally {
4961
+ unmark();
3491
4962
  }
3492
4963
  let judged = null;
3493
- const status = runMutationResult(dir, null, io2, part ? chosen.selected : [], (seen) => {
3494
- judged = seen;
3495
- });
4964
+ const status = runMutationResult(
4965
+ dir,
4966
+ null,
4967
+ io2,
4968
+ part ? chosen.selected : [],
4969
+ (seen) => {
4970
+ judged = seen;
4971
+ },
4972
+ retried
4973
+ );
3496
4974
  guard.record(judged);
3497
4975
  if (part) {
3498
- fs8.rmSync(reportFile, { force: true });
4976
+ fs10.rmSync(reportFile, { force: true });
3499
4977
  }
3500
4978
  return status;
3501
4979
  }
4980
+ function retryTimeouts(stryker, args, manifest, reportFile, gates, io2) {
4981
+ let first;
4982
+ try {
4983
+ first = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
4984
+ } catch {
4985
+ return null;
4986
+ }
4987
+ if (typeof first !== "object" || first === null) {
4988
+ return null;
4989
+ }
4990
+ const { ranges, rerun } = timeoutRanges(first, gates);
4991
+ if (ranges.length === 0 || rerun <= manifest.settings.maxTimeouts) {
4992
+ return null;
4993
+ }
4994
+ io2.out(
4995
+ `test-gates mutation: ${rerun} mutant(s) timed out outside expectedTimeouts, running them again one at a time (--concurrency 1)`
4996
+ );
4997
+ fs10.rmSync(reportFile, { force: true });
4998
+ const result = spawnSync4(
4999
+ stryker,
5000
+ ["run", ...args, "--mutate", ranges.join(","), "--concurrency", "1", "--force"],
5001
+ { cwd: manifest.dir, stdio: "inherit", shell: process.platform === "win32" }
5002
+ );
5003
+ let retry = null;
5004
+ if (!result.error && result.status === 0) {
5005
+ try {
5006
+ retry = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
5007
+ } catch {
5008
+ retry = null;
5009
+ }
5010
+ }
5011
+ if (retry === null || typeof retry !== "object") {
5012
+ const why = result.error ? result.error.message : result.signal ? `signal ${result.signal}` : result.status === 0 ? "no report" : `exit ${result.status}`;
5013
+ io2.err(
5014
+ `test-gates mutation: running the timed-out mutants again failed (${why}). The first run is judged`
5015
+ );
5016
+ fs10.writeFileSync(reportFile, JSON.stringify(first));
5017
+ return "failed";
5018
+ }
5019
+ const merged = mergeRetry(first, retry, gates);
5020
+ fs10.writeFileSync(reportFile, JSON.stringify(merged.report));
5021
+ io2.out(formatRetry(merged.outcome));
5022
+ return merged.outcome;
5023
+ }
3502
5024
 
3503
5025
  // src/commands/doctor.ts
3504
5026
  function read(file) {
3505
5027
  try {
3506
- return fs9.readFileSync(file, "utf8");
5028
+ return fs11.readFileSync(file, "utf8");
3507
5029
  } catch {
3508
5030
  return null;
3509
5031
  }
3510
5032
  }
3511
- function git2(dir, args) {
3512
- const result = spawnSync3("git", args, {
5033
+ function git3(dir, args) {
5034
+ const result = spawnSync5("git", args, {
3513
5035
  cwd: dir,
3514
5036
  env: gitEnvironment(process.env),
3515
5037
  encoding: "utf8"
@@ -3517,20 +5039,20 @@ function git2(dir, args) {
3517
5039
  return result.status === 0 ? result.stdout.trim() : null;
3518
5040
  }
3519
5041
  function hooksOf(dir) {
3520
- const root = git2(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
3521
- const hooksDir = git2(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
5042
+ const root = git3(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
5043
+ const hooksDir = git3(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
3522
5044
  const candidates = [
3523
- ...["pre-push", "pre-commit"].map((name) => path9.join(root, ".husky", name)),
3524
- ...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path9.join(hooksDir, name)),
3525
- ...["lefthook.yml", ".lefthook.yml"].map((name) => path9.join(root, name))
5045
+ ...["pre-push", "pre-commit"].map((name) => path11.join(root, ".husky", name)),
5046
+ ...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path11.join(hooksDir, name)),
5047
+ ...["lefthook.yml", ".lefthook.yml"].map((name) => path11.join(root, name))
3526
5048
  ];
3527
5049
  let script = "";
3528
5050
  try {
3529
- const manifest = JSON.parse(read(path9.join(dir, "package.json")) ?? "{}");
5051
+ const manifest = JSON.parse(read(path11.join(dir, "package.json")) ?? "{}");
3530
5052
  script = manifest.scripts?.["test:gates:mutation"] ?? "";
3531
5053
  } catch {
3532
5054
  }
3533
- const subproject = path9.relative(fs9.realpathSync(root), dir).split(path9.sep).join("/");
5055
+ const subproject = path11.relative(fs11.realpathSync(root), dir).split(path11.sep).join("/");
3534
5056
  const seen = /* @__PURE__ */ new Set();
3535
5057
  const uses = [];
3536
5058
  for (const file of candidates) {
@@ -3538,12 +5060,12 @@ function hooksOf(dir) {
3538
5060
  if (text === null) {
3539
5061
  continue;
3540
5062
  }
3541
- const real = fs9.realpathSync(file);
5063
+ const real = fs11.realpathSync(file);
3542
5064
  if (seen.has(real)) {
3543
5065
  continue;
3544
5066
  }
3545
5067
  seen.add(real);
3546
- const shown = path9.relative(root, file).split(path9.sep).join("/");
5068
+ const shown = path11.relative(root, file).split(path11.sep).join("/");
3547
5069
  uses.push(...hookUses({ file: shown, text, subproject, script }));
3548
5070
  }
3549
5071
  return uses;
@@ -3560,8 +5082,8 @@ function runDoctor(dir, io2) {
3560
5082
  const affected = decideReuse(manifest, config, [], false, true);
3561
5083
  io2.out(`test-gates doctor: ${manifest.dir} (${config})`);
3562
5084
  if (plain2.snapshot !== null) {
3563
- const subproject = path9.relative(plain2.root, fs9.realpathSync(manifest.dir));
3564
- const prefix = subproject === "" ? "" : `${subproject.split(path9.sep).join("/")}/`;
5085
+ const subproject = path11.relative(plain2.root, fs11.realpathSync(manifest.dir));
5086
+ const prefix = subproject === "" ? "" : `${subproject.split(path11.sep).join("/")}/`;
3565
5087
  const keys = Object.keys(plain2.snapshot.files);
3566
5088
  const outside = keys.filter((key) => !key.startsWith(prefix)).length;
3567
5089
  const followed = plain2.scope === null ? "" : "files" in plain2.scope ? `, paths followed in the ${plain2.scope.files} files the run loads` : ", paths followed in every file";
@@ -3572,6 +5094,15 @@ function runDoctor(dir, io2) {
3572
5094
  io2.out(
3573
5095
  reuse === null ? " next run: reuses the results of the last run" : ` next run: every mutant, ${reuse}`
3574
5096
  );
5097
+ if (plain2.seed !== null) {
5098
+ io2.out(
5099
+ ` this worktree has no record of its own: the next run starts from the one of the main worktree (${plain2.mainWorktree})`
5100
+ );
5101
+ } else if (plain2.mainWorktree !== null) {
5102
+ io2.out(
5103
+ ` this worktree has no record of its own, and the main worktree (${plain2.mainWorktree}) has none this worktree can start from`
5104
+ );
5105
+ }
3575
5106
  if (affected.ci !== null) {
3576
5107
  io2.out(` next run with --affected: every mutant, ${affected.ci}`);
3577
5108
  } else if (affected.plan !== null && "rerun" in affected.plan) {
@@ -3588,15 +5119,20 @@ function runDoctor(dir, io2) {
3588
5119
  }
3589
5120
  }
3590
5121
  let blind = null;
5122
+ let blindGates = [];
3591
5123
  if (plain2.snapshot !== null) {
3592
5124
  try {
3593
- affectedInputs(manifest, {
5125
+ const inputs = affectedInputs(manifest, {
3594
5126
  root: plain2.root,
3595
5127
  files: plain2.snapshot.files,
3596
5128
  configs: plain2.configs(),
3597
5129
  tests: [],
3598
5130
  covering: {}
3599
5131
  });
5132
+ blindGates = manifest.gates.flatMap((gate) => {
5133
+ const reason = inputs.gates[gate.path]?.blind;
5134
+ return reason === void 0 ? [] : [{ gate: gate.path, reason }];
5135
+ });
3600
5136
  } catch (error) {
3601
5137
  if (!(error instanceof AffectedError)) {
3602
5138
  throw error;
@@ -3604,6 +5140,8 @@ function runDoctor(dir, io2) {
3604
5140
  blind = error.message;
3605
5141
  }
3606
5142
  }
5143
+ const left = sandboxPlan(manifest.dir, plain2.temp);
5144
+ const count = "kept" in left.plan ? left.plan.kept.length : left.plan.remove.length;
3607
5145
  const { lines, status } = formatFindings(
3608
5146
  diagnose({
3609
5147
  incremental: plain2.incremental,
@@ -3611,8 +5149,16 @@ function runDoctor(dir, io2) {
3611
5149
  failure: plain2.failure,
3612
5150
  widened: plain2.widened,
3613
5151
  scope: plain2.scope,
3614
- hooks: hooksOf(fs9.realpathSync(manifest.dir)),
3615
- blind
5152
+ hooks: hooksOf(fs11.realpathSync(manifest.dir)),
5153
+ blind,
5154
+ blindGates,
5155
+ gates: manifest.gates.length,
5156
+ sandboxes: count === 0 ? null : {
5157
+ tempDir: left.shown,
5158
+ count,
5159
+ kept: "kept" in left.plan ? left.plan.why : null,
5160
+ running: "kept" in left.plan ? left.plan.running : null
5161
+ }
3616
5162
  })
3617
5163
  );
3618
5164
  lines.forEach((line) => io2.out(line));
@@ -3620,8 +5166,8 @@ function runDoctor(dir, io2) {
3620
5166
  }
3621
5167
 
3622
5168
  // src/commands/lcov.ts
3623
- import fs10 from "fs";
3624
- import path10 from "path";
5169
+ import fs12 from "fs";
5170
+ import path12 from "path";
3625
5171
 
3626
5172
  // src/core/lcov.ts
3627
5173
  function normalizePath2(file, rootDir) {
@@ -3648,9 +5194,9 @@ function parseLcov(text, rootDir) {
3648
5194
  const line = rawLine.trim();
3649
5195
  if (line.startsWith("SF:")) {
3650
5196
  closeRecord();
3651
- const path13 = normalizePath2(line.slice(3), rootDir);
3652
- const file = files.get(path13) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
3653
- files.set(path13, file);
5197
+ const path15 = normalizePath2(line.slice(3), rootDir);
5198
+ const file = files.get(path15) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
5199
+ files.set(path15, file);
3654
5200
  record = { file, hasDa: false, found: 0, hit: 0 };
3655
5201
  } else if (line === "end_of_record") {
3656
5202
  closeRecord();
@@ -3734,10 +5280,10 @@ function runLcov(dir, fileOption, io2) {
3734
5280
  "no lcov file given. Pass --file <lcov.info> or set settings.lcov.file"
3735
5281
  );
3736
5282
  }
3737
- const lcovPath = path10.resolve(manifest.dir, file);
5283
+ const lcovPath = path12.resolve(manifest.dir, file);
3738
5284
  let text;
3739
5285
  try {
3740
- text = fs10.readFileSync(lcovPath, "utf8");
5286
+ text = fs12.readFileSync(lcovPath, "utf8");
3741
5287
  } catch (error) {
3742
5288
  io2.err(
3743
5289
  `test-gates lcov: cannot read ${file} (${error.message}). Run the tests with coverage first`
@@ -3780,10 +5326,10 @@ function runLcov(dir, fileOption, io2) {
3780
5326
  }
3781
5327
 
3782
5328
  // src/commands/selfcheck.ts
3783
- import { spawn, spawnSync as spawnSync4 } from "child_process";
3784
- import fs12 from "fs";
5329
+ import { spawn, spawnSync as spawnSync6 } from "child_process";
5330
+ import fs14 from "fs";
3785
5331
  import os2 from "os";
3786
- import path12 from "path";
5332
+ import path14 from "path";
3787
5333
 
3788
5334
  // src/core/jobs.ts
3789
5335
  var SWITCHES = {
@@ -3823,6 +5369,34 @@ function knownCommand(program, args) {
3823
5369
  function jobFlags(runner, dir) {
3824
5370
  return runner === "jest" ? [`--coverageDirectory=${dir}/coverage`, `--cacheDirectory=${dir}/cache`] : [`--coverage.reportsDirectory=${dir}/coverage`, "--no-cache"];
3825
5371
  }
5372
+ var LIMITS = {
5373
+ jest: ["--runInBand", "-i", "--maxWorkers", "-w"],
5374
+ vitest: ["--no-file-parallelism", "--fileParallelism", "--maxWorkers", "--minWorkers"]
5375
+ };
5376
+ var CONFIG_LIMITS = [
5377
+ "maxWorkers",
5378
+ "minWorkers",
5379
+ "maxThreads",
5380
+ "minThreads",
5381
+ "maxForks",
5382
+ "minForks",
5383
+ "fileParallelism",
5384
+ "singleThread",
5385
+ "singleFork"
5386
+ ];
5387
+ function workerFlags(input) {
5388
+ if (input.lines <= 1) {
5389
+ return [];
5390
+ }
5391
+ const given = input.args.some(
5392
+ (arg) => LIMITS[input.runner].includes(arg.split("=")[0])
5393
+ );
5394
+ const configured = CONFIG_LIMITS.some((word) => input.configSource?.includes(word) === true);
5395
+ if (given || configured) {
5396
+ return [];
5397
+ }
5398
+ return [`--maxWorkers=${Math.max(1, Math.floor(input.cores / input.lines))}`];
5399
+ }
3826
5400
  var REACHES = [
3827
5401
  "process.",
3828
5402
  "process?.",
@@ -3854,10 +5428,22 @@ var REACHES = [
3854
5428
  ];
3855
5429
  var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])|\bimport\.meta\.glob\b/;
3856
5430
  var COMPUTED = "loads a module whose name is computed";
5431
+ var STRING = /(^|[:,([={?+&|;])(\s*)(['"])((?:\\.|(?!\3)[^\\\n])*)\3(?=\s*(?:[,}\]);:+?&|]|$))/gm;
5432
+ function withoutStrings(source) {
5433
+ return source.replace(
5434
+ STRING,
5435
+ (all, before, space, quote, inside) => (
5436
+ // In a template (`{"home": "${home()}"}`) the quotes are text, and what is
5437
+ // between them may be code.
5438
+ inside.includes("${") ? all : `${before}${space}${quote}${" ".repeat(inside.length)}${quote}`
5439
+ )
5440
+ );
5441
+ }
3857
5442
  var RUNNER_PACKAGES = ["vitest", "@jest/globals"];
3858
5443
  function reachesOutside(file, trusted) {
3859
5444
  const dense = file.source.replace(/\s/g, "");
3860
- const mark = REACHES.find((text) => dense.includes(text));
5445
+ const code = withoutStrings(file.source).replace(/\s/g, "");
5446
+ const mark = REACHES.find((text) => code.includes(text));
3861
5447
  if (mark !== void 0) {
3862
5448
  return `uses ${mark.replace(/[.?[(:]+$/, "")}`;
3863
5449
  }
@@ -4000,8 +5586,8 @@ function cleanHold(input) {
4000
5586
 
4001
5587
  // src/touch.ts
4002
5588
  import { createHash as createHash2 } from "crypto";
4003
- import fs11 from "fs";
4004
- import path11 from "path";
5589
+ import fs13 from "fs";
5590
+ import path13 from "path";
4005
5591
  import { fileURLToPath } from "url";
4006
5592
  var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
4007
5593
  function isObject(value) {
@@ -4035,7 +5621,7 @@ function isCounters(value) {
4035
5621
  function readNotes(directory) {
4036
5622
  const read2 = (file) => {
4037
5623
  try {
4038
- return JSON.parse(fs11.readFileSync(path11.join(directory, file), "utf8"));
5624
+ return JSON.parse(fs13.readFileSync(path13.join(directory, file), "utf8"));
4039
5625
  } catch {
4040
5626
  return null;
4041
5627
  }
@@ -4043,8 +5629,8 @@ function readNotes(directory) {
4043
5629
  const run = read2("run.json");
4044
5630
  let notes = [];
4045
5631
  try {
4046
- for (const name of fs11.readdirSync(path11.join(directory, "specs"))) {
4047
- const note = read2(path11.join("specs", name));
5632
+ for (const name of fs13.readdirSync(path13.join(directory, "specs"))) {
5633
+ const note = read2(path13.join("specs", name));
4048
5634
  if (!isObject(note) || typeof note.spec !== "string" || !isObject(note.files) || !Object.values(note.files).every(isCounters)) {
4049
5635
  throw new Error("not a note");
4050
5636
  }
@@ -4056,7 +5642,7 @@ function readNotes(directory) {
4056
5642
  return {
4057
5643
  run: isObject(run) && typeof run.runner === "string" && typeof run.version === "string" && typeof run.provider === "string" && typeof run.pool === "string" && typeof run.isolate === "boolean" && typeof run.projects === "number" && (run.problem === null || typeof run.problem === "string") ? run : null,
4058
5644
  notes,
4059
- totals: countersOfAll(read2(path11.join("coverage", "coverage-final.json")))
5645
+ totals: countersOfAll(read2(path13.join("coverage", "coverage-final.json")))
4060
5646
  };
4061
5647
  }
4062
5648
 
@@ -4071,7 +5657,7 @@ function resolveGateCommand(manifest) {
4071
5657
  if (manifest.settings.gateCommand !== null) {
4072
5658
  return manifest.settings.gateCommand;
4073
5659
  }
4074
- const exists = (name) => fs12.existsSync(path12.join(manifest.dir, name));
5660
+ const exists = (name) => fs14.existsSync(path14.join(manifest.dir, name));
4075
5661
  const jest = JEST_CONFIGS.find(exists);
4076
5662
  const vitest = VITEST_CONFIGS.find(exists);
4077
5663
  if (jest && vitest) {
@@ -4092,7 +5678,7 @@ function resolveGateCommand(manifest) {
4092
5678
  var TOUCH_PREFIX = ".test-gates-touch-";
4093
5679
  function realPath(file) {
4094
5680
  try {
4095
- return fs12.realpathSync(file);
5681
+ return fs14.realpathSync(file);
4096
5682
  } catch {
4097
5683
  return file;
4098
5684
  }
@@ -4100,13 +5686,16 @@ function realPath(file) {
4100
5686
  function shareOneRun(manifest, targets, runGate) {
4101
5687
  let touchDir;
4102
5688
  try {
4103
- for (const name of fs12.readdirSync(manifest.dir)) {
4104
- const left = path12.join(manifest.dir, name);
4105
- if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
4106
- fs12.rmSync(left, { recursive: true, force: true });
5689
+ for (const name of fs14.readdirSync(manifest.dir)) {
5690
+ const left = path14.join(manifest.dir, name);
5691
+ try {
5692
+ if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
5693
+ fs14.rmSync(left, { recursive: true, force: true });
5694
+ }
5695
+ } catch {
4107
5696
  }
4108
5697
  }
4109
- touchDir = fs12.mkdtempSync(path12.join(manifest.dir, TOUCH_PREFIX));
5698
+ touchDir = fs14.mkdtempSync(path14.join(manifest.dir, TOUCH_PREFIX));
4110
5699
  } catch {
4111
5700
  return { reason: "the subproject cannot be written to" };
4112
5701
  }
@@ -4117,14 +5706,14 @@ function shareOneRun(manifest, targets, runGate) {
4117
5706
  return { reason: "the gate run with every spec does not pass" };
4118
5707
  }
4119
5708
  const base = realPath(manifest.dir);
4120
- const absolute = (file) => realPath(path12.resolve(base, file));
5709
+ const absolute = (file) => realPath(path14.resolve(base, file));
4121
5710
  const rekeyed = (files) => Object.fromEntries(Object.entries(files).map(([file, value]) => [absolute(file), value]));
4122
5711
  const left = readNotes(touchDir);
4123
5712
  plan = planControls({
4124
5713
  all: manifest.gates.map((gate) => ({
4125
5714
  path: gate.path,
4126
- file: path12.join(base, gate.path),
4127
- specFile: path12.join(base, gate.spec ?? "")
5715
+ file: path14.join(base, gate.path),
5716
+ specFile: path14.join(base, gate.spec ?? "")
4128
5717
  })),
4129
5718
  targets: targets.map((gate) => gate.path),
4130
5719
  run: left.run,
@@ -4133,7 +5722,7 @@ function shareOneRun(manifest, targets, runGate) {
4133
5722
  failurePattern: manifest.settings.selfcheckFailurePattern
4134
5723
  });
4135
5724
  } finally {
4136
- fs12.rmSync(touchDir, { recursive: true, force: true });
5725
+ fs14.rmSync(touchDir, { recursive: true, force: true });
4137
5726
  }
4138
5727
  if (plan.reason !== null) {
4139
5728
  return { reason: plan.reason };
@@ -4143,15 +5732,15 @@ function shareOneRun(manifest, targets, runGate) {
4143
5732
  return { gates, result: runGate({ excludeAll: gates.map((gate) => gate.spec) }) };
4144
5733
  }
4145
5734
  function installedVersion2(start, name) {
4146
- let current = path12.resolve(start);
5735
+ let current = path14.resolve(start);
4147
5736
  for (; ; ) {
4148
5737
  try {
4149
5738
  const manifest = JSON.parse(
4150
- fs12.readFileSync(path12.join(current, "node_modules", name, "package.json"), "utf8")
5739
+ fs14.readFileSync(path14.join(current, "node_modules", name, "package.json"), "utf8")
4151
5740
  );
4152
5741
  return typeof manifest.version === "string" ? manifest.version : null;
4153
5742
  } catch {
4154
- const parent = path12.dirname(current);
5743
+ const parent = path14.dirname(current);
4155
5744
  if (parent === current) {
4156
5745
  return null;
4157
5746
  }
@@ -4180,24 +5769,39 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
4180
5769
  const results = /* @__PURE__ */ new Map();
4181
5770
  let root;
4182
5771
  try {
4183
- for (const name of fs12.readdirSync(os2.tmpdir())) {
4184
- const left = path12.join(os2.tmpdir(), name);
4185
- if (name.startsWith(JOBS_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
4186
- fs12.rmSync(left, { recursive: true, force: true });
5772
+ root = fs14.realpathSync(fs14.mkdtempSync(path14.join(os2.tmpdir(), JOBS_PREFIX)));
5773
+ } catch {
5774
+ return results;
5775
+ }
5776
+ try {
5777
+ for (const name of fs14.readdirSync(os2.tmpdir())) {
5778
+ const left = path14.join(os2.tmpdir(), name);
5779
+ try {
5780
+ if (name.startsWith(JOBS_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
5781
+ fs14.rmSync(left, { recursive: true, force: true });
5782
+ }
5783
+ } catch {
4187
5784
  }
4188
5785
  }
4189
- root = fs12.realpathSync(fs12.mkdtempSync(path12.join(os2.tmpdir(), JOBS_PREFIX)));
4190
5786
  } catch {
4191
- return results;
4192
5787
  }
4193
5788
  const queue = [...gates];
5789
+ const lines = Math.min(jobs, gates.length);
5790
+ const workers = workerFlags({
5791
+ runner,
5792
+ args,
5793
+ configSource: gateConfigSource(manifest, args),
5794
+ lines,
5795
+ // (os.availableParallelism is in Node 18.14 and later.)
5796
+ cores: os2.availableParallelism?.() ?? os2.cpus().length
5797
+ });
4194
5798
  const line = async (dir) => {
4195
5799
  for (let gate = queue.shift(); gate !== void 0; gate = queue.shift()) {
4196
5800
  const spec = gate.spec;
4197
5801
  results.set(
4198
5802
  gate,
4199
5803
  await new Promise((resolve) => {
4200
- const child = spawn(executable, [...args, ...jobFlags(runner, dir)], {
5804
+ const child = spawn(executable, [...args, ...jobFlags(runner, dir), ...workers], {
4201
5805
  cwd: manifest.dir,
4202
5806
  env: { ...process.env, [EXCLUDE_ENV]: spec, [EXCLUDE_ALL_ENV]: "", [TOUCH_ENV]: "" },
4203
5807
  shell: process.platform === "win32"
@@ -4216,13 +5820,10 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
4216
5820
  };
4217
5821
  try {
4218
5822
  await Promise.all(
4219
- Array.from(
4220
- { length: Math.min(jobs, gates.length) },
4221
- (_unused, index) => line(path12.join(root, `line-${index}`))
4222
- )
5823
+ Array.from({ length: lines }, (_unused, index) => line(path14.join(root, `line-${index}`)))
4223
5824
  );
4224
5825
  } finally {
4225
- fs12.rmSync(root, { recursive: true, force: true });
5826
+ fs14.rmSync(root, { recursive: true, force: true });
4226
5827
  }
4227
5828
  return results;
4228
5829
  }
@@ -4251,7 +5852,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
4251
5852
  const command = commandOverride.length > 0 ? commandOverride : resolveGateCommand(manifest);
4252
5853
  const [program, ...args] = command;
4253
5854
  const executable = findLocalBin(manifest.dir, program) ?? program;
4254
- const runGate = (env) => spawnSync4(executable, args, {
5855
+ const runGate = (env) => spawnSync6(executable, args, {
4255
5856
  cwd: manifest.dir,
4256
5857
  env: {
4257
5858
  ...process.env,
@@ -4329,6 +5930,11 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
4329
5930
  ...manifest.gates.flatMap((gate) => [gate.path, gate.spec])
4330
5931
  ].sort((left, right) => right.length - left.length);
4331
5932
  const results = await runAtOnce(manifest, atOnce, jobs, known.runner, executable, args);
5933
+ if (results.size === 0) {
5934
+ io2.out(
5935
+ `test-gates selfcheck: one negative control at a time (no directory for the runs next to others could be made in ${os2.tmpdir()})`
5936
+ );
5937
+ }
4332
5938
  for (const [gate, result] of results) {
4333
5939
  const output = `${result.stdout}${result.stderr}`;
4334
5940
  const clean = cleanHold({
@@ -4346,7 +5952,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
4346
5952
  }
4347
5953
  }
4348
5954
  const again = atOnce.length - heldAtOnce.size;
4349
- if (again > 0) {
5955
+ if (again > 0 && results.size > 0) {
4350
5956
  io2.out(
4351
5957
  `test-gates selfcheck: ${again} of ${atOnce.length} negative control(s) did not hold cleanly next to others and run again on their own`
4352
5958
  );