@t09tanaka/test-gates 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@ function parseShard(value) {
28
28
  }
29
29
  var OPTIONS_BY_COMMAND = {
30
30
  check: ["--dir"],
31
- mutation: ["--dir", "--gate", "--affected"],
31
+ mutation: ["--dir", "--gate", "--shard", "--affected"],
32
32
  "mutation-result": ["--dir", "--report"],
33
33
  selfcheck: ["--dir", "--all", "--first", "--gate", "--shard", "--quick", "--jobs"],
34
34
  lcov: ["--dir", "--file"],
@@ -146,13 +146,18 @@ function parseArgs(argv) {
146
146
  "--affected and --gate cannot be combined: --gate runs the gates it names, --affected decides which gates run"
147
147
  );
148
148
  }
149
+ if (parsed.affected && parsed.shard !== null) {
150
+ throw new UsageError(
151
+ "--affected and --shard cannot be combined: --shard runs every mutant of its gates, --affected decides which gates run"
152
+ );
153
+ }
149
154
  const misplaced = parsed.rest.find((arg) => /^--(gate|shard|quick|jobs)(=|$)/.test(arg));
150
155
  if (parsed.command === "selfcheck" && misplaced !== void 0) {
151
156
  throw new UsageError(
152
157
  `${misplaced.split("=")[0]} after "--" would go to the gate command. Write it before "--"`
153
158
  );
154
159
  }
155
- const forStryker = parsed.rest.find((arg) => /^--(gate|affected)(=|$)/.test(arg));
160
+ const forStryker = parsed.rest.find((arg) => /^--(gate|shard|affected)(=|$)/.test(arg));
156
161
  if (forStryker !== void 0) {
157
162
  throw new UsageError(
158
163
  `${forStryker.split("=")[0]} after "--" would go to Stryker. Write it before "--"`
@@ -1034,13 +1039,13 @@ function judgeSelfcheck(input) {
1034
1039
  function selectControls(input) {
1035
1040
  const { gates, mode, only, shard } = input;
1036
1041
  if (only.length > 0) {
1037
- const unknown = only.filter((path13) => !gates.includes(path13));
1042
+ const unknown = only.filter((path15) => !gates.includes(path15));
1038
1043
  if (unknown.length > 0) {
1039
1044
  return {
1040
1045
  error: `--gate ${unknown.join(", ")}: not a gate of test-gates.json (write the path as it is written there)`
1041
1046
  };
1042
1047
  }
1043
- return { selected: gates.filter((path13) => only.includes(path13)) };
1048
+ return { selected: gates.filter((path15) => only.includes(path15)) };
1044
1049
  }
1045
1050
  if (shard !== null) {
1046
1051
  if (shard.total > gates.length) {
@@ -1083,6 +1088,12 @@ function gateRunRefused(env) {
1083
1088
  function affectedRefused(env) {
1084
1089
  return inCi(env) ? "CI is set, and there every mutant is run (the final check)" : null;
1085
1090
  }
1091
+ function shardRunCaveat(shard, judged, all) {
1092
+ if (shard.total === 1) {
1093
+ return `. All ${all} gate(s) were run and judged (--shard 1/1 is the only part)`;
1094
+ }
1095
+ return `. Only part ${shard.index}/${shard.total} (${judged} of ${all} gate(s)) was run and judged (--shard): every part has to pass for the final check`;
1096
+ }
1086
1097
  function gateRunCaveat(judged, all) {
1087
1098
  return `. Only ${judged} of ${all} gate(s) were run and judged (--gate), and a change in one gate can make a mutant of another survive: run without --gate for the final check`;
1088
1099
  }
@@ -1193,8 +1204,8 @@ function planControls(input) {
1193
1204
  if (silent !== void 0) {
1194
1205
  return everyoneAlone(`the gate run left no note for the spec of ${silent.path}`);
1195
1206
  }
1196
- const partsOf2 = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
1197
- const short = all.find((gate) => accountedFor(totals[gate.file], partsOf2(gate)) === "no");
1207
+ const partsOf3 = (gate) => notes.flatMap((note) => gate.file in note.files ? [note.files[gate.file]] : []);
1208
+ const short = all.find((gate) => accountedFor(totals[gate.file], partsOf3(gate)) === "no");
1198
1209
  if (short !== void 0) {
1199
1210
  return everyoneAlone(
1200
1211
  `the notes of the specs do not add up to what the runner counted for ${short.path}`
@@ -1208,7 +1219,7 @@ function planControls(input) {
1208
1219
  if (all.some((other) => other.path !== gate.path && other.path.includes(gate.path))) {
1209
1220
  return false;
1210
1221
  }
1211
- if (accountedFor(totals[gate.file], partsOf2(gate)) !== "yes") {
1222
+ if (accountedFor(totals[gate.file], partsOf3(gate)) !== "yes") {
1212
1223
  return false;
1213
1224
  }
1214
1225
  const touching = notes.filter((note) => {
@@ -1419,9 +1430,9 @@ function runCheck(dir, io2) {
1419
1430
  }
1420
1431
 
1421
1432
  // src/commands/doctor.ts
1422
- import { spawnSync as spawnSync3 } from "child_process";
1423
- import fs9 from "fs";
1424
- import path9 from "path";
1433
+ import { spawnSync as spawnSync5 } from "child_process";
1434
+ import fs11 from "fs";
1435
+ import path11 from "path";
1425
1436
 
1426
1437
  // src/core/mutation-result.ts
1427
1438
  var DETECTED = /* @__PURE__ */ new Set(["Killed", "Timeout"]);
@@ -1688,6 +1699,80 @@ function judgeMutationReport(input) {
1688
1699
  }
1689
1700
  return { violations, survivors, unexpectedTimeouts, summary };
1690
1701
  }
1702
+ var UNKNOWN_TEST = "not-in-the-first-run:";
1703
+ function sameMutant(a, b) {
1704
+ 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;
1705
+ }
1706
+ function mergeRetry(first, retry, gates) {
1707
+ const outcome = { retried: 0, statuses: {}, missing: 0 };
1708
+ const firstId = translator(first, retry);
1709
+ const files = { ...first.files };
1710
+ const again = retry.files ?? {};
1711
+ for (const named of gates) {
1712
+ const gate = named.path.split("/").filter((part) => part !== ".").join("/");
1713
+ const result = Object.hasOwn(files, gate) ? files[gate] : void 0;
1714
+ if (result?.mutants === void 0) {
1715
+ continue;
1716
+ }
1717
+ const expected = expectedTimeoutMutants(named, result.source, result.mutants);
1718
+ const rerun = Object.hasOwn(again, gate) ? again[gate]?.mutants : void 0;
1719
+ files[gate] = {
1720
+ ...result,
1721
+ mutants: result.mutants.map((mutant) => {
1722
+ if (mutant.status !== "Timeout" || expected.has(mutant)) {
1723
+ return mutant;
1724
+ }
1725
+ outcome.retried += 1;
1726
+ const found = rerun?.find((other) => sameMutant(other, mutant));
1727
+ if (found === void 0) {
1728
+ outcome.missing += 1;
1729
+ return mutant;
1730
+ }
1731
+ outcome.statuses[found.status] = (outcome.statuses[found.status] ?? 0) + 1;
1732
+ return {
1733
+ ...found,
1734
+ ...found.coveredBy === void 0 ? {} : { coveredBy: found.coveredBy.map(firstId) },
1735
+ ...found.killedBy === void 0 ? {} : { killedBy: found.killedBy.map(firstId) }
1736
+ };
1737
+ })
1738
+ };
1739
+ }
1740
+ return { report: { ...first, files }, outcome };
1741
+ }
1742
+ function testKey(file, test) {
1743
+ const start = test.location?.start;
1744
+ return JSON.stringify([file, test.name ?? "", start?.line, start?.column]);
1745
+ }
1746
+ function* keyedTests(report) {
1747
+ for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
1748
+ if (entry.tests !== void 0) {
1749
+ for (const test of entry.tests) {
1750
+ yield { id: test.id, key: testKey(file, test) };
1751
+ }
1752
+ }
1753
+ }
1754
+ }
1755
+ function translator(first, retry) {
1756
+ const firstIds = /* @__PURE__ */ new Map();
1757
+ for (const { id, key } of keyedTests(first)) {
1758
+ firstIds.set(key, firstIds.has(key) ? null : id);
1759
+ }
1760
+ const ids = /* @__PURE__ */ new Map();
1761
+ for (const { id, key } of keyedTests(retry)) {
1762
+ ids.set(id, firstIds.get(key));
1763
+ }
1764
+ return (id) => {
1765
+ const found = ids.get(id);
1766
+ return typeof found === "string" ? found : `${UNKNOWN_TEST}${id}`;
1767
+ };
1768
+ }
1769
+ function formatRetry(outcome) {
1770
+ const statuses = Object.entries(outcome.statuses).sort(([a], [b]) => a.localeCompare(b)).map(([status, count]) => `${count} ${status}`);
1771
+ if (outcome.missing > 0) {
1772
+ statuses.push(`${outcome.missing} not in the report (still Timeout)`);
1773
+ }
1774
+ return `test-gates mutation: ${outcome.retried} mutant(s) that timed out were run again one at a time (--concurrency 1): ${statuses.join(", ")}`;
1775
+ }
1691
1776
  function formatSurvivor(survivor) {
1692
1777
  const occurrence = survivor.sameKeyCount > 1 ? ` / occurrence ${survivor.occurrence} of ${survivor.sameKeyCount}` : "";
1693
1778
  return `${survivor.file}:${survivor.line}:${survivor.column} / ${survivor.mutator} / ${survivor.original} \u2192 ${survivor.replacement}${occurrence} (${survivor.status})`;
@@ -1697,7 +1782,7 @@ function formatSummary(summary) {
1697
1782
  }
1698
1783
 
1699
1784
  // src/core/reuse-guard.ts
1700
- var RECORD_FORMAT = 2;
1785
+ var RECORD_FORMAT = 3;
1701
1786
  function isStringRecord(value) {
1702
1787
  return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
1703
1788
  }
@@ -1709,11 +1794,14 @@ function parseAffected(value) {
1709
1794
  }
1710
1795
  const parsed = /* @__PURE__ */ Object.create(null);
1711
1796
  for (const [gate, entry] of Object.entries(gates)) {
1712
- const { specs, files } = entry ?? {};
1797
+ const { specs, files, blind } = entry ?? {};
1713
1798
  if (!isStringList(specs) || !isStringList(files)) {
1714
1799
  return null;
1715
1800
  }
1716
- parsed[gate] = { specs, files };
1801
+ if (blind !== void 0 && typeof blind !== "string") {
1802
+ return null;
1803
+ }
1804
+ parsed[gate] = { specs, files, ...blind === void 0 ? {} : { blind } };
1717
1805
  }
1718
1806
  return { tests, gates: parsed };
1719
1807
  }
@@ -1876,7 +1964,8 @@ var CONFIG_NAMES = [
1876
1964
  /\.d\.[cm]?ts$/
1877
1965
  ];
1878
1966
  var ROOT_MOCKS = "__mocks__";
1879
- var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc)$/i;
1967
+ var DOCUMENT = /\.(?:md|markdown|txt|rst|adoc|html?)$/i;
1968
+ var ENTRY_PAGE = /^index\.html?$/i;
1880
1969
  var PASSIVE = /\.(?:[cm]?[jt]sx?|json|snap|css|scss|sass|less|svg|png|jpe?g|gif|webp|avif|ico)$/i;
1881
1970
  function kindOf(key, subproject) {
1882
1971
  const segments = localSegments(key, subproject);
@@ -1887,7 +1976,7 @@ function kindOf(key, subproject) {
1887
1976
  if (isNestedMock(key, subproject)) {
1888
1977
  return "other";
1889
1978
  }
1890
- if (DOCUMENT.test(name)) {
1979
+ if (DOCUMENT.test(name) && !ENTRY_PAGE.test(name)) {
1891
1980
  return "document";
1892
1981
  }
1893
1982
  const inside = subproject === "" || key.startsWith(`${subproject}/`);
@@ -1904,6 +1993,7 @@ function isNestedMock(key, subproject) {
1904
1993
  }
1905
1994
  function gateDependencies(input) {
1906
1995
  const closureOf = (key) => Object.hasOwn(input.closures, key) ? input.closures[key] : [key];
1996
+ const blind = input.blind ?? {};
1907
1997
  const result = /* @__PURE__ */ Object.create(null);
1908
1998
  for (const gate of input.gates) {
1909
1999
  const covering = Object.hasOwn(input.covering, gate.key) ? input.covering[gate.key] : [];
@@ -1917,7 +2007,13 @@ function gateDependencies(input) {
1917
2007
  for (const spec of specs) {
1918
2008
  closureOf(spec).forEach((file) => files.add(file));
1919
2009
  }
1920
- result[gate.key] = { specs: [...specs].sort(), files: [...files].sort() };
2010
+ const sorted2 = [...files].sort();
2011
+ const reason = sorted2.find((file) => Object.hasOwn(blind, file));
2012
+ result[gate.key] = {
2013
+ specs: [...specs].sort(),
2014
+ files: sorted2,
2015
+ ...reason === void 0 ? {} : { blind: blind[reason] }
2016
+ };
1921
2017
  }
1922
2018
  return result;
1923
2019
  }
@@ -1948,6 +2044,15 @@ function planAffected(input) {
1948
2044
  rerun.push({ gate: gate.path, changed: [] });
1949
2045
  continue;
1950
2046
  }
2047
+ const blind = gate.blind ?? last.blind;
2048
+ if (blind !== void 0) {
2049
+ if (changed.length > 0 || input.seeded === true) {
2050
+ rerun.push({ gate: gate.path, changed, blind });
2051
+ } else {
2052
+ reused.push(gate.path);
2053
+ }
2054
+ continue;
2055
+ }
1951
2056
  const files = [.../* @__PURE__ */ new Set([...last.files, ...gate.files])];
1952
2057
  const differing = files.filter((file) => input.before.files[file] !== input.now[file]).sort();
1953
2058
  if (differing.length > 0) {
@@ -1999,13 +2104,23 @@ function describePlan(plan, shown = 5, timeouts = 0, expected = 0) {
1999
2104
  const lines = [
2000
2105
  `test-gates mutation --affected: ${plan.reused.length} of ${total} gate(s) reused, ${plan.rerun.length} run again`
2001
2106
  ];
2002
- for (const { gate, changed } of plan.rerun.slice(0, shown)) {
2107
+ const told = plan.rerun.filter((entry) => entry.blind === void 0);
2108
+ for (const { gate, changed } of told.slice(0, shown)) {
2003
2109
  lines.push(
2004
2110
  changed.length === 0 ? ` ${gate}: the last run left no result of it that can be reused` : ` ${gate}: changed ${listed(changed)}`
2005
2111
  );
2006
2112
  }
2007
- if (plan.rerun.length > shown) {
2008
- lines.push(` and ${plan.rerun.length - shown} more`);
2113
+ if (told.length > shown) {
2114
+ lines.push(` and ${told.length - shown} more`);
2115
+ }
2116
+ const reasons = [
2117
+ ...new Set(plan.rerun.flatMap((entry) => entry.blind === void 0 ? [] : [entry.blind]))
2118
+ ];
2119
+ if (reasons.length > 0) {
2120
+ const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
2121
+ lines.push(
2122
+ ` ${plan.rerun.length - told.length} gate(s) run again for any change, as what they depend on cannot be told: ${reasons[0]}${more}`
2123
+ );
2009
2124
  }
2010
2125
  if (timeouts > 0) {
2011
2126
  lines.push(` and ${timeouts} mutant(s) of reused gates that timed out in the last run`);
@@ -2052,12 +2167,22 @@ function diagnose(facts) {
2052
2167
  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"
2053
2168
  });
2054
2169
  }
2170
+ const everyBlind = facts.blindGates.length >= facts.gates;
2055
2171
  if (facts.blind !== null) {
2056
2172
  findings.push({
2057
2173
  level: facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
2058
2174
  text: `with --affected, a change of any file runs every mutant: what a gate depends on cannot be told (${facts.blind})`,
2059
2175
  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"
2060
2176
  });
2177
+ } else if (facts.blindGates.length > 0) {
2178
+ const gates = facts.blindGates.map((one) => one.gate);
2179
+ const reasons = [...new Set(facts.blindGates.map((one) => one.reason))];
2180
+ const more = reasons.length > 1 ? ` (and ${reasons.length - 1} more reason(s))` : "";
2181
+ findings.push({
2182
+ level: everyBlind && facts.hooks.some((hook) => hook.affected) ? "problem" : "note",
2183
+ 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})`,
2184
+ fix: "keep what reads files by a path no import names out of what those gates and their specs import"
2185
+ });
2061
2186
  }
2062
2187
  if (facts.scope !== null && "every" in facts.scope) {
2063
2188
  findings.push({
@@ -2066,6 +2191,21 @@ function diagnose(facts) {
2066
2191
  fix: facts.scope.every.startsWith(NOT_THE_HELPER) ? "make the runner config with createJestGatesConfig or createVitestGatesConfig" : null
2067
2192
  });
2068
2193
  }
2194
+ if (facts.sandboxes !== null) {
2195
+ const { tempDir, count, kept, running } = facts.sandboxes;
2196
+ findings.push(
2197
+ running === null ? {
2198
+ level: "note",
2199
+ 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`,
2200
+ fix: kept === null ? "the next test-gates mutation removes them" : `remove them by hand; the next test-gates mutation keeps them, because ${kept}`
2201
+ } : {
2202
+ // The sandboxes may be those of that run: removing them would break it.
2203
+ level: "note",
2204
+ text: `another test-gates mutation is running in this subproject (pid ${running}): the ${count} sandbox(es) in ${tempDir} are in use`,
2205
+ fix: "leave them; the first test-gates mutation after that run ends removes what is left"
2206
+ }
2207
+ );
2208
+ }
2069
2209
  if (facts.hooks.length === 0) {
2070
2210
  findings.push({
2071
2211
  level: "note",
@@ -2073,7 +2213,8 @@ function diagnose(facts) {
2073
2213
  fix: null
2074
2214
  });
2075
2215
  }
2076
- for (const hook of facts.hooks.filter((one) => !one.affected && facts.blind === null)) {
2216
+ const toldApart = facts.blind === null && !everyBlind;
2217
+ for (const hook of facts.hooks.filter((one) => !one.affected && toldApart)) {
2077
2218
  findings.push({
2078
2219
  level: "problem",
2079
2220
  text: `${hook.file} runs test-gates mutation without --affected: a change of any file of the subproject runs every mutant`,
@@ -2099,39 +2240,322 @@ function formatFindings(findings) {
2099
2240
  var RUNS_MUTATION = /(?<![\w-])test-gates(?:\.js)?\s+mutation(?![\w-])|(?<![\w:-])test:gates:mutation(?![\w:-])/;
2100
2241
  var TARGET = /(?:--dir|--prefix|--workspace|-w)(?:=|\s+)(['"]?)([^\s'";&|)]+)\1/;
2101
2242
  var CD = /\bcd\s+(['"]?)([^\s'";&|)]+)\1/;
2243
+ var NONE = [];
2244
+ var COMMAND = String.raw`(?<=(?:^|[\n;&|(){]|\b(?:then|else|do))\s*)`;
2245
+ var TOKEN = new RegExp(
2246
+ String.raw`\n|;;&?|;&|${COMMAND}(?:case\s+(\S+)\s+in|for\s+(\w+)(?:\s+in\s+([^;\n]*))?|(esac|do|done))(?![\w-])`,
2247
+ "g"
2248
+ );
2249
+ var PATTERN = /\s*(?:esac(?![\w-])|\(?([^()\n;]*)\))/y;
2250
+ var VARIABLE = /^("?)\$(?:(\w+)|\{(\w+)\})\1$/;
2251
+ var REFERENCE = /\$(?:(\w+)|\{(\w+)\})/g;
2252
+ function namesOf(words) {
2253
+ const names = words.map((word) => word.trim().replace(/^(["'])(.*)\1$/, "$2"));
2254
+ return names.some((name) => name === "" || /[$*?[{}~\\`"']/.test(name)) ? null : names;
2255
+ }
2256
+ function partsOf(text) {
2257
+ const parts = [];
2258
+ let scopes = NONE;
2259
+ let start = 0;
2260
+ let loop = null;
2261
+ const enter = (scope) => {
2262
+ scopes = [...scopes, scope];
2263
+ };
2264
+ const leave = (kind) => {
2265
+ const left = scopes.at(-1)?.kind === kind;
2266
+ scopes = scopes.slice(0, -1);
2267
+ return left;
2268
+ };
2269
+ TOKEN.lastIndex = 0;
2270
+ for (; ; ) {
2271
+ const token = TOKEN.exec(text);
2272
+ parts.push({ text: text.slice(start, token?.index), scopes });
2273
+ if (token === null) {
2274
+ return scopes.length === 0 ? parts : null;
2275
+ }
2276
+ const [word, subject, variable, list, keyword] = token;
2277
+ let awaiting = false;
2278
+ if (subject !== void 0) {
2279
+ const named = VARIABLE.exec(subject);
2280
+ enter({
2281
+ kind: "case",
2282
+ id: /* @__PURE__ */ Symbol(),
2283
+ variable: named?.[2] ?? named?.[3] ?? null,
2284
+ names: null
2285
+ });
2286
+ awaiting = true;
2287
+ } else if (variable !== void 0) {
2288
+ loop = {
2289
+ kind: "loop",
2290
+ id: /* @__PURE__ */ Symbol(),
2291
+ variable,
2292
+ names: list === void 0 ? null : namesOf(list.trim().split(/\s+/))
2293
+ };
2294
+ } else if (keyword === "do") {
2295
+ enter(loop ?? { kind: "loop", id: /* @__PURE__ */ Symbol(), variable: null, names: null });
2296
+ loop = null;
2297
+ } else if (keyword === "done" || keyword === "esac") {
2298
+ if (!leave(keyword === "done" ? "loop" : "case")) {
2299
+ return null;
2300
+ }
2301
+ } else if (word === ";&") {
2302
+ return null;
2303
+ } else if (word !== "\n") {
2304
+ if (scopes.at(-1)?.kind !== "case") {
2305
+ return null;
2306
+ }
2307
+ awaiting = true;
2308
+ }
2309
+ start = TOKEN.lastIndex;
2310
+ if (awaiting) {
2311
+ PATTERN.lastIndex = start;
2312
+ const pattern = PATTERN.exec(text);
2313
+ if (pattern === null) {
2314
+ return null;
2315
+ }
2316
+ start = PATTERN.lastIndex;
2317
+ TOKEN.lastIndex = start;
2318
+ const top = scopes.at(-1);
2319
+ scopes = scopes.slice(0, -1);
2320
+ if (pattern[1] !== void 0) {
2321
+ enter({ ...top, names: namesOf(pattern[1].split("|")) });
2322
+ }
2323
+ }
2324
+ }
2325
+ }
2326
+ function sibling(of, scopes) {
2327
+ const apart = of.findIndex((scope, at) => scope !== scopes[at]);
2328
+ return apart !== -1 && of[apart].id === scopes[apart]?.id;
2329
+ }
2330
+ function expansions(target, scopes) {
2331
+ return [...target.matchAll(REFERENCE)].reduce(
2332
+ (words, [reference, plain2, braced]) => {
2333
+ const names = [...scopes].reverse().find((scope) => scope.variable === (plain2 ?? braced))?.names ?? [reference];
2334
+ return words.flatMap((word) => names.map((name) => word.replace(reference, name)));
2335
+ },
2336
+ [target]
2337
+ );
2338
+ }
2102
2339
  function hookUses(input) {
2103
2340
  const lines = input.text.split("\n").map((line) => line.replace(/(?:^|\s)#.*/, ""));
2341
+ const parts = partsOf(lines.join("\n")) ?? lines.map((text) => ({ text, scopes: NONE }));
2104
2342
  const uses = [];
2105
- lines.forEach((line, index) => {
2106
- if (!RUNS_MUTATION.test(line)) {
2343
+ const cds = parts.map((part) => ({ cd: CD.exec(part.text)?.[2], scopes: part.scopes }));
2344
+ parts.forEach((part, index) => {
2345
+ if (!RUNS_MUTATION.test(part.text)) {
2107
2346
  return;
2108
2347
  }
2109
- const named = TARGET.exec(line) ?? CD.exec(line);
2110
- const target = named?.[2] ?? lines.slice(0, index).reverse().map((before) => CD.exec(before)?.[2]).find((found) => found !== void 0);
2111
- const normalized = target?.split("/").filter((part) => part !== "." && part !== "").join("/");
2112
- const ours = normalized === void 0 || normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
2348
+ const named = TARGET.exec(part.text) ?? CD.exec(part.text);
2349
+ const before = cds.slice(0, index).reverse().find((one) => one.cd !== void 0 && !sibling(one.scopes, part.scopes));
2350
+ const target = named?.[2] ?? before?.cd;
2351
+ const scopes = named !== null || before?.scopes === part.scopes ? part.scopes : NONE;
2352
+ const ours = target === void 0 || expansions(target, scopes).some((word) => {
2353
+ const normalized = word.split("/").filter((one) => one !== "." && one !== "").join("/");
2354
+ return normalized.includes("$") || normalized.includes("..") || normalized === input.subproject;
2355
+ });
2113
2356
  if (ours) {
2114
2357
  uses.push({
2115
2358
  file: input.file,
2116
- affected: /--affected\b/.test(line) || /test:gates:mutation\b/.test(line) && input.script.includes("--affected")
2359
+ affected: /--affected\b/.test(part.text) || /test:gates:mutation\b/.test(part.text) && input.script.includes("--affected")
2117
2360
  });
2118
2361
  }
2119
2362
  });
2120
2363
  return uses;
2121
2364
  }
2122
2365
 
2123
- // src/snapshot.ts
2366
+ // src/sandboxes.ts
2124
2367
  import { spawnSync } from "child_process";
2368
+ import fs4 from "fs";
2369
+ import path4 from "path";
2370
+
2371
+ // src/core/sandboxes.ts
2372
+ var SANDBOX = /^sandbox-[A-Za-z0-9]{6}$/;
2373
+ var MARKER = /^test-gates-run-([1-9][0-9]*)$/;
2374
+ function markerName(pid) {
2375
+ return `test-gates-run-${pid}`;
2376
+ }
2377
+ function markerText(pid, started) {
2378
+ return `${pid}
2379
+ ${started ?? ""}
2380
+ `;
2381
+ }
2382
+ function markerAlive(input) {
2383
+ const recorded = input.text?.split("\n")[1]?.trim() || null;
2384
+ return input.there && (recorded === null || input.started === null || sameStart(recorded, input.started));
2385
+ }
2386
+ var START_SLACK_SECONDS = 5;
2387
+ function sameStart(a, b) {
2388
+ const difference = Math.abs(Date.parse(a) - Date.parse(b));
2389
+ return Number.isNaN(difference) ? a === b : difference <= START_SLACK_SECONDS * 1e3;
2390
+ }
2391
+ function insideSubproject(tempDir) {
2392
+ const parts = tempDir.split("/");
2393
+ return !/^[A-Za-z]:/.test(tempDir) && parts.every((part) => part !== "" && part !== "." && part !== "..");
2394
+ }
2395
+ function planSandboxes(facts) {
2396
+ const sandboxes = facts.entries.filter((entry) => entry.directory && SANDBOX.test(entry.name)).map((entry) => entry.name).sort();
2397
+ if (facts.tempDir === null) {
2398
+ return {
2399
+ kept: sandboxes,
2400
+ why: "the Stryker config could not be loaded to see where they are",
2401
+ running: null
2402
+ };
2403
+ }
2404
+ if (!insideSubproject(facts.tempDir)) {
2405
+ return {
2406
+ kept: sandboxes,
2407
+ why: `tempDirName (${facts.tempDir}) is not a directory inside the subproject`,
2408
+ running: null
2409
+ };
2410
+ }
2411
+ if (facts.keeps) {
2412
+ return { kept: sandboxes, why: "the Stryker config sets cleanTempDir: false", running: null };
2413
+ }
2414
+ const markers = [];
2415
+ for (const entry of facts.entries) {
2416
+ const pid = MARKER.exec(entry.name)?.[1];
2417
+ if (pid === void 0 || entry.directory) {
2418
+ continue;
2419
+ }
2420
+ if (facts.alive(Number(pid), entry.name)) {
2421
+ return {
2422
+ kept: sandboxes,
2423
+ why: `another test-gates mutation is running in this subproject (pid ${pid})`,
2424
+ running: Number(pid)
2425
+ };
2426
+ }
2427
+ markers.push(entry.name);
2428
+ }
2429
+ return { remove: sandboxes, markers };
2430
+ }
2431
+
2432
+ // src/sandboxes.ts
2433
+ function there(pid) {
2434
+ try {
2435
+ process.kill(pid, 0);
2436
+ return true;
2437
+ } catch (error) {
2438
+ return error.code === "EPERM";
2439
+ }
2440
+ }
2441
+ function startedAt(pid) {
2442
+ const result = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
2443
+ encoding: "utf8",
2444
+ env: { ...process.env, LC_ALL: "C" },
2445
+ timeout: 5e3
2446
+ });
2447
+ const started = result.status === 0 ? result.stdout.trim() : "";
2448
+ return started === "" ? null : started;
2449
+ }
2450
+ function alive(dir) {
2451
+ return (pid, marker) => {
2452
+ if (!there(pid)) {
2453
+ return false;
2454
+ }
2455
+ let text = null;
2456
+ try {
2457
+ text = fs4.readFileSync(path4.join(dir, marker), "utf8");
2458
+ } catch {
2459
+ }
2460
+ return markerAlive({ there: true, text, started: startedAt(pid) });
2461
+ };
2462
+ }
2463
+ function relativeTempDir(subproject, dir) {
2464
+ let real = dir;
2465
+ try {
2466
+ real = fs4.realpathSync(dir);
2467
+ } catch {
2468
+ }
2469
+ return path4.relative(fs4.realpathSync(subproject), real).split(path4.sep).join("/");
2470
+ }
2471
+ function sandboxPlan(subproject, setting) {
2472
+ const tempDir = setting.dir === null ? null : relativeTempDir(subproject, setting.dir);
2473
+ let entries = [];
2474
+ if (setting.dir !== null) {
2475
+ try {
2476
+ entries = fs4.readdirSync(setting.dir, { withFileTypes: true });
2477
+ } catch {
2478
+ }
2479
+ }
2480
+ const plan = planSandboxes({
2481
+ tempDir,
2482
+ entries: entries.map((entry) => ({ name: entry.name, directory: entry.isDirectory() })),
2483
+ keeps: setting.keeps,
2484
+ alive: alive(setting.dir ?? subproject)
2485
+ });
2486
+ return { plan, shown: tempDir ?? ".stryker-tmp" };
2487
+ }
2488
+ function removeStaleSandboxes(subproject, setting) {
2489
+ const { plan, shown } = sandboxPlan(subproject, setting);
2490
+ if ("kept" in plan) {
2491
+ if (plan.kept.length === 0) {
2492
+ return [];
2493
+ }
2494
+ return plan.running === null ? [
2495
+ `test-gates mutation: ${plan.kept.length} sandbox(es) left in ${shown} by an earlier Stryker run are kept: ${plan.why}`
2496
+ ] : [
2497
+ `test-gates mutation: ${plan.why}, so the ${plan.kept.length} sandbox(es) in ${shown} are kept: they are in use`
2498
+ ];
2499
+ }
2500
+ const dir = setting.dir;
2501
+ for (const marker of plan.markers) {
2502
+ try {
2503
+ fs4.rmSync(path4.join(dir, marker), { force: true });
2504
+ } catch {
2505
+ }
2506
+ }
2507
+ const failed = [];
2508
+ for (const sandbox of plan.remove) {
2509
+ try {
2510
+ fs4.rmSync(path4.join(dir, sandbox), { recursive: true, force: true });
2511
+ } catch (error) {
2512
+ failed.push(`${sandbox} (${error.message})`);
2513
+ }
2514
+ }
2515
+ const removed = plan.remove.length - failed.length;
2516
+ return [
2517
+ ...removed > 0 ? [
2518
+ `test-gates mutation: removed ${removed} sandbox(es) an earlier Stryker run left in ${shown}`
2519
+ ] : [],
2520
+ ...failed.length > 0 ? [`test-gates mutation: could not remove from ${shown}: ${failed.join(", ")}`] : []
2521
+ ];
2522
+ }
2523
+ function markRun(subproject, setting) {
2524
+ const dir = setting.dir;
2525
+ if (dir === null || !insideSubproject(relativeTempDir(subproject, dir))) {
2526
+ return () => void 0;
2527
+ }
2528
+ const marker = path4.join(dir, markerName(process.pid));
2529
+ try {
2530
+ fs4.mkdirSync(dir, { recursive: true });
2531
+ if (!insideSubproject(relativeTempDir(subproject, dir))) {
2532
+ return () => void 0;
2533
+ }
2534
+ fs4.writeFileSync(marker, markerText(process.pid, startedAt(process.pid)));
2535
+ } catch {
2536
+ return () => void 0;
2537
+ }
2538
+ return () => {
2539
+ try {
2540
+ fs4.rmSync(marker, { force: true });
2541
+ fs4.rmdirSync(dir);
2542
+ } catch {
2543
+ }
2544
+ };
2545
+ }
2546
+
2547
+ // src/snapshot.ts
2548
+ import { spawnSync as spawnSync2 } from "child_process";
2125
2549
  import { createHash } from "crypto";
2126
- import fs5 from "fs";
2550
+ import fs6 from "fs";
2127
2551
  import { createRequire } from "module";
2128
2552
  import os from "os";
2129
- import path5 from "path";
2553
+ import path6 from "path";
2130
2554
 
2131
2555
  // src/commands/loaded-files.ts
2132
- import fs4 from "fs";
2556
+ import fs5 from "fs";
2133
2557
  import { builtinModules } from "module";
2134
- import path4 from "path";
2558
+ import path5 from "path";
2135
2559
  var SCRIPT = /\.[cm]?[jt]sx?$/;
2136
2560
  var COMPONENT = /\.(?:vue|svelte|astro|mdx)$/;
2137
2561
  var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
@@ -2139,7 +2563,7 @@ var QUOTED = /(['"`])([^'"`\n]+)\1/g;
2139
2563
  var RUNNER_LOAD = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|createMockFromModule|genMockFromModule)\s*(?:<[^()\n]*>)?\s*\(\s*(['"`])([^'"`\n]+)\1/g;
2140
2564
  function isFile(file) {
2141
2565
  try {
2142
- return fs4.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
2566
+ return fs5.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
2143
2567
  } catch {
2144
2568
  return false;
2145
2569
  }
@@ -2172,9 +2596,9 @@ function filesOf(base, directory, declarations) {
2172
2596
  ...extensions.map((extension) => base + extension),
2173
2597
  // `./fee.js` written in a TypeScript file is `./fee.ts`
2174
2598
  ...extensions.map((extension) => base.replace(/\.[cm]?jsx?$/, extension)),
2175
- ...directory ? extensions.map((extension) => path4.join(base, `index${extension}`)) : []
2599
+ ...directory ? extensions.map((extension) => path5.join(base, `index${extension}`)) : []
2176
2600
  ];
2177
- const data = [`${base}.json`, ...directory ? [path4.join(base, "index.json")] : []];
2601
+ const data = [`${base}.json`, ...directory ? [path5.join(base, "index.json")] : []];
2178
2602
  const scripts = [.../* @__PURE__ */ new Set([base, ...candidates(EXTENSIONS), ...data])].filter(isFile);
2179
2603
  return scripts.length > 0 || !declarations ? scripts : [...new Set(candidates(DECLARATIONS))].filter(isFile);
2180
2604
  }
@@ -2183,11 +2607,11 @@ function resolveFile(base, directory = true, declarations = true) {
2183
2607
  }
2184
2608
  function installed(specifier, from) {
2185
2609
  const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
2186
- for (let current = path4.dirname(from); ; current = path4.dirname(current)) {
2187
- if (fs4.existsSync(path4.join(current, "node_modules", name))) {
2610
+ for (let current = path5.dirname(from); ; current = path5.dirname(current)) {
2611
+ if (fs5.existsSync(path5.join(current, "node_modules", name))) {
2188
2612
  return true;
2189
2613
  }
2190
- if (current === path4.dirname(current)) {
2614
+ if (current === path5.dirname(current)) {
2191
2615
  return false;
2192
2616
  }
2193
2617
  }
@@ -2197,18 +2621,18 @@ function loadedFiles(manifest, configFile) {
2197
2621
  }
2198
2622
  function loadedFilesNow(manifest, configFile) {
2199
2623
  const aliases = manifest.settings.importAliases;
2200
- const relative = (file) => path4.relative(manifest.dir, file).split(path4.sep).join("/");
2624
+ const relative = (file) => path5.relative(manifest.dir, file).split(path5.sep).join("/");
2201
2625
  const target = (specifier, from) => {
2202
2626
  if (specifier.startsWith(".")) {
2203
- return resolveFiles(path4.resolve(path4.dirname(from), specifier));
2627
+ return resolveFiles(path5.resolve(path5.dirname(from), specifier));
2204
2628
  }
2205
2629
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
2206
2630
  return alias === void 0 ? [] : resolveFiles(
2207
- path4.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
2631
+ path5.resolve(manifest.dir, alias.target + specifier.slice(alias.prefix.length))
2208
2632
  );
2209
2633
  };
2210
2634
  const read2 = (file) => {
2211
- const source = fs4.readFileSync(file, "utf8");
2635
+ const source = fs5.readFileSync(file, "utf8");
2212
2636
  const imports = parseImports(source).filter((ref) => !ref.typeOnly);
2213
2637
  const specifiers = [
2214
2638
  .../* @__PURE__ */ new Set([
@@ -2220,7 +2644,7 @@ function loadedFilesNow(manifest, configFile) {
2220
2644
  return { source, specifiers, strings };
2221
2645
  };
2222
2646
  const named = /* @__PURE__ */ new Set();
2223
- const configs = configFile === null ? [] : [path4.resolve(manifest.dir, configFile)];
2647
+ const configs = configFile === null ? [] : [path5.resolve(manifest.dir, configFile)];
2224
2648
  const seenConfigs = new Set(configs);
2225
2649
  const entry = configs[0];
2226
2650
  for (let file = configs.shift(); file !== void 0; file = configs.shift()) {
@@ -2228,8 +2652,8 @@ function loadedFilesNow(manifest, configFile) {
2228
2652
  const paths = strings.filter((text) => file === entry || /^(?:\.|<rootDir>\/)/.test(text));
2229
2653
  for (const text of paths) {
2230
2654
  const stripped = text.replace(/^<rootDir>\//, "");
2231
- const here = resolveFiles(path4.resolve(path4.dirname(file), stripped), false);
2232
- const found = here.length > 0 ? here : resolveFiles(path4.resolve(manifest.dir, stripped), false);
2655
+ const here = resolveFiles(path5.resolve(path5.dirname(file), stripped), false);
2656
+ const found = here.length > 0 ? here : resolveFiles(path5.resolve(manifest.dir, stripped), false);
2233
2657
  for (const one of found.filter((candidate) => SCRIPT.test(candidate))) {
2234
2658
  named.add(one);
2235
2659
  }
@@ -2270,7 +2694,7 @@ function loadedFilesNow(manifest, configFile) {
2270
2694
  }
2271
2695
  }
2272
2696
  for (const text of strings.filter((one) => one.startsWith("."))) {
2273
- next.push(...resolveFiles(path4.resolve(path4.dirname(file), text)));
2697
+ next.push(...resolveFiles(path5.resolve(path5.dirname(file), text)));
2274
2698
  }
2275
2699
  const visited = {
2276
2700
  file: { path: relative(file), source, imports },
@@ -2300,7 +2724,7 @@ function loadedFilesNow(manifest, configFile) {
2300
2724
  const specs = [...new Set(manifest.gates.flatMap((gate) => gate.existingSpecs))];
2301
2725
  const roots = [
2302
2726
  closure(null, [...named]),
2303
- ...specs.map((spec) => closure(spec, [path4.join(manifest.dir, spec)]))
2727
+ ...specs.map((spec) => closure(spec, [path5.join(manifest.dir, spec)]))
2304
2728
  ];
2305
2729
  return { files: [...known.values()].map((visited) => visited.file), roots };
2306
2730
  }
@@ -2352,17 +2776,17 @@ function hashOf(content) {
2352
2776
  }
2353
2777
  function hashFile(file) {
2354
2778
  try {
2355
- return hashOf(fs5.readFileSync(file));
2779
+ return hashOf(fs6.readFileSync(file));
2356
2780
  } catch {
2357
2781
  return null;
2358
2782
  }
2359
2783
  }
2360
2784
  function isInside(parent, child) {
2361
- const relative = path5.relative(parent, child);
2362
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
2785
+ const relative = path6.relative(parent, child);
2786
+ return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
2363
2787
  }
2364
2788
  function toPosix(file) {
2365
- return file.split(path5.sep).join("/");
2789
+ return file.split(path6.sep).join("/");
2366
2790
  }
2367
2791
  function gitEnvironment(env) {
2368
2792
  if (env.GIT_DIR === void 0 || env.GIT_WORK_TREE !== void 0) {
@@ -2373,7 +2797,7 @@ function gitEnvironment(env) {
2373
2797
  return rest;
2374
2798
  }
2375
2799
  function git(cwd, args) {
2376
- const result = spawnSync("git", args, {
2800
+ const result = spawnSync2("git", args, {
2377
2801
  cwd,
2378
2802
  env: gitEnvironment(process.env),
2379
2803
  encoding: "utf8",
@@ -2386,7 +2810,7 @@ function globLiteral(relative) {
2386
2810
  }
2387
2811
  function gitLister(root) {
2388
2812
  return (directory, recursive) => {
2389
- const relative = globLiteral(toPosix(path5.relative(root, directory)));
2813
+ const relative = globLiteral(toPosix(path6.relative(root, directory)));
2390
2814
  const pattern = recursive ? `${relative === "" ? "" : `${relative}/`}**` : `${relative === "" ? "" : `${relative}/`}*`;
2391
2815
  const listed2 = git(root, [
2392
2816
  "ls-files",
@@ -2405,8 +2829,8 @@ function gitLister(root) {
2405
2829
  }
2406
2830
  function walkLister(root) {
2407
2831
  const walk = (directory, recursive, found) => {
2408
- for (const entry of fs5.readdirSync(directory, { withFileTypes: true })) {
2409
- const file = path5.join(directory, entry.name);
2832
+ for (const entry of fs6.readdirSync(directory, { withFileTypes: true })) {
2833
+ const file = path6.join(directory, entry.name);
2410
2834
  if (NEVER.includes(entry.name)) {
2411
2835
  continue;
2412
2836
  }
@@ -2415,7 +2839,7 @@ function walkLister(root) {
2415
2839
  walk(file, true, found);
2416
2840
  }
2417
2841
  } else {
2418
- found.push(toPosix(path5.relative(root, file)));
2842
+ found.push(toPosix(path6.relative(root, file)));
2419
2843
  }
2420
2844
  }
2421
2845
  return found;
@@ -2425,16 +2849,16 @@ function walkLister(root) {
2425
2849
  function upTo(from, to) {
2426
2850
  const directories = [from];
2427
2851
  while (directories[directories.length - 1] !== to) {
2428
- directories.push(path5.dirname(directories[directories.length - 1]));
2852
+ directories.push(path6.dirname(directories[directories.length - 1]));
2429
2853
  }
2430
2854
  return directories;
2431
2855
  }
2432
2856
  function declaresWorkspaces(directory) {
2433
- if (fs5.existsSync(path5.join(directory, "pnpm-workspace.yaml"))) {
2857
+ if (fs6.existsSync(path6.join(directory, "pnpm-workspace.yaml"))) {
2434
2858
  return true;
2435
2859
  }
2436
2860
  try {
2437
- const manifest = JSON.parse(fs5.readFileSync(path5.join(directory, "package.json"), "utf8"));
2861
+ const manifest = JSON.parse(fs6.readFileSync(path6.join(directory, "package.json"), "utf8"));
2438
2862
  return manifest.workspaces !== void 0;
2439
2863
  } catch {
2440
2864
  return false;
@@ -2442,7 +2866,7 @@ function declaresWorkspaces(directory) {
2442
2866
  }
2443
2867
  function walkRoot(dir) {
2444
2868
  const candidates = withinBounds(dir);
2445
- const repository = candidates.find((directory) => fs5.existsSync(path5.join(directory, ".git")));
2869
+ const repository = candidates.find((directory) => fs6.existsSync(path6.join(directory, ".git")));
2446
2870
  if (repository !== void 0) {
2447
2871
  return repository;
2448
2872
  }
@@ -2451,11 +2875,11 @@ function walkRoot(dir) {
2451
2875
  function withinBounds(dir) {
2452
2876
  let home = null;
2453
2877
  try {
2454
- home = fs5.realpathSync(os.homedir());
2878
+ home = fs6.realpathSync(os.homedir());
2455
2879
  } catch {
2456
2880
  }
2457
- return upTo(dir, path5.parse(dir).root).filter(
2458
- (directory) => directory !== path5.parse(dir).root && (home === null || !isInside(directory, home))
2881
+ return upTo(dir, path6.parse(dir).root).filter(
2882
+ (directory) => directory !== path6.parse(dir).root && (home === null || !isInside(directory, home))
2459
2883
  );
2460
2884
  }
2461
2885
  function linkedPackages(nodeModules, root) {
@@ -2463,20 +2887,20 @@ function linkedPackages(nodeModules, root) {
2463
2887
  const visit = (directory, depth) => {
2464
2888
  let entries;
2465
2889
  try {
2466
- entries = fs5.readdirSync(directory, { withFileTypes: true });
2890
+ entries = fs6.readdirSync(directory, { withFileTypes: true });
2467
2891
  } catch {
2468
2892
  return;
2469
2893
  }
2470
2894
  for (const entry of entries) {
2471
- const file = path5.join(directory, entry.name);
2895
+ const file = path6.join(directory, entry.name);
2472
2896
  if (entry.isSymbolicLink()) {
2473
2897
  let target;
2474
2898
  try {
2475
- target = fs5.realpathSync(file);
2899
+ target = fs6.realpathSync(file);
2476
2900
  } catch {
2477
2901
  continue;
2478
2902
  }
2479
- if (target.split(path5.sep).includes("node_modules") || !fs5.statSync(target).isDirectory()) {
2903
+ if (target.split(path6.sep).includes("node_modules") || !fs6.statSync(target).isDirectory()) {
2480
2904
  continue;
2481
2905
  }
2482
2906
  if (!isInside(root, target)) {
@@ -2498,11 +2922,11 @@ function installedVersion(start, name) {
2498
2922
  for (; ; ) {
2499
2923
  try {
2500
2924
  const manifest = JSON.parse(
2501
- fs5.readFileSync(path5.join(current, "node_modules", name, "package.json"), "utf8")
2925
+ fs6.readFileSync(path6.join(current, "node_modules", name, "package.json"), "utf8")
2502
2926
  );
2503
2927
  return typeof manifest.version === "string" ? manifest.version : "unknown";
2504
2928
  } catch {
2505
- const parent = path5.dirname(current);
2929
+ const parent = path6.dirname(current);
2506
2930
  if (parent === current) {
2507
2931
  return null;
2508
2932
  }
@@ -2511,7 +2935,7 @@ function installedVersion(start, name) {
2511
2935
  }
2512
2936
  }
2513
2937
  function nodeOnPath(cwd) {
2514
- const result = spawnSync("node", ["--version"], {
2938
+ const result = spawnSync2("node", ["--version"], {
2515
2939
  cwd,
2516
2940
  encoding: "utf8",
2517
2941
  shell: process.platform === "win32",
@@ -2528,12 +2952,12 @@ function snapshotOf(input) {
2528
2952
  return resolving(() => snapshotNow(input));
2529
2953
  }
2530
2954
  function snapshotNow(input) {
2531
- const dir = fs5.realpathSync(input.dir);
2955
+ const dir = fs6.realpathSync(input.dir);
2532
2956
  const top = git(dir, ["rev-parse", "--show-toplevel"]);
2533
- let root = top === null ? dir : fs5.realpathSync(top.trim());
2957
+ let root = top === null ? dir : fs6.realpathSync(top.trim());
2534
2958
  let lister = gitLister(root);
2535
2959
  let mode = "git";
2536
- const manifest = toPosix(path5.relative(root, path5.join(dir, "test-gates.json")));
2960
+ const manifest = toPosix(path6.relative(root, path6.join(dir, "test-gates.json")));
2537
2961
  if (top === null || !lister(dir, false).includes(manifest)) {
2538
2962
  mode = "walk";
2539
2963
  root = walkRoot(dir);
@@ -2541,17 +2965,17 @@ function snapshotNow(input) {
2541
2965
  }
2542
2966
  const listRoot = root;
2543
2967
  const list = lister;
2544
- const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path5.join(listRoot, file)));
2968
+ const followed = input.scope === void 0 ? null : input.scope(list(dir, true).map((file) => path6.join(listRoot, file)));
2545
2969
  const walk = walkLister(listRoot);
2546
2970
  const written = input.written.map((file) => {
2547
- const target = path5.resolve(dir, file);
2971
+ const target = path6.resolve(dir, file);
2548
2972
  try {
2549
- return fs5.realpathSync(target);
2973
+ return fs6.realpathSync(target);
2550
2974
  } catch {
2551
2975
  return target;
2552
2976
  }
2553
2977
  });
2554
- const swallowing = written.find((target) => isInside(target, path5.join(dir, "test-gates.json")));
2978
+ const swallowing = written.find((target) => isInside(target, path6.join(dir, "test-gates.json")));
2555
2979
  if (swallowing !== void 0) {
2556
2980
  throw new SnapshotError(`the run writes to ${swallowing}, which holds test-gates.json`);
2557
2981
  }
@@ -2568,12 +2992,12 @@ function snapshotNow(input) {
2568
2992
  if (!isInside(listRoot, directory)) {
2569
2993
  const written2 = reference === void 0 ? "" : ` with ${JSON.stringify(reference)}`;
2570
2994
  throw new SnapshotError(
2571
- `${toPosix(path5.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
2995
+ `${toPosix(path6.relative(dir, from))} points outside of what can be listed${written2} (${directory})`
2572
2996
  );
2573
2997
  }
2574
2998
  if (directory === listRoot && listRoot !== dir && widened.length < WIDENED_SHOWN) {
2575
2999
  const written2 = reference === void 0 ? "" : ` ${JSON.stringify(reference)}`;
2576
- widened.push(`${toPosix(path5.relative(listRoot, from))}${written2}`);
3000
+ widened.push(`${toPosix(path6.relative(listRoot, from))}${written2}`);
2577
3001
  }
2578
3002
  if (![...queued].some((region) => isInside(region, directory))) {
2579
3003
  queued.add(directory);
@@ -2582,10 +3006,10 @@ function snapshotNow(input) {
2582
3006
  };
2583
3007
  const follow = (fixed, open, anchored, file, reference) => {
2584
3008
  const from = (start) => {
2585
- const target = path5.resolve(start, fixed);
3009
+ const target = path6.resolve(start, fixed);
2586
3010
  let isDirectory2 = false;
2587
3011
  try {
2588
- isDirectory2 = fs5.statSync(target).isDirectory();
3012
+ isDirectory2 = fs6.statSync(target).isDirectory();
2589
3013
  } catch {
2590
3014
  }
2591
3015
  return {
@@ -2594,9 +3018,9 @@ function snapshotNow(input) {
2594
3018
  found: isDirectory2 || resolveFile(target, true, false) !== null
2595
3019
  };
2596
3020
  };
2597
- const fromFile = from(path5.dirname(file));
3021
+ const fromFile = from(path6.dirname(file));
2598
3022
  const targets = [fromFile];
2599
- if (!anchored && path5.dirname(file) !== dir) {
3023
+ if (!anchored && path6.dirname(file) !== dir) {
2600
3024
  const fromRun = from(dir);
2601
3025
  if (fromRun.found && !fromFile.found) {
2602
3026
  targets[0] = fromRun;
@@ -2615,24 +3039,24 @@ function snapshotNow(input) {
2615
3039
  continue;
2616
3040
  }
2617
3041
  if (!isDirectory2 && !isInside(listRoot, target) && resolveFile(target, true, false) === null) {
2618
- files[toPosix(path5.relative(listRoot, target))] = "missing";
3042
+ files[toPosix(path6.relative(listRoot, target))] = "missing";
2619
3043
  continue;
2620
3044
  }
2621
- const directory = isDirectory2 ? target : path5.dirname(target);
2622
- if (fs5.existsSync(directory)) {
3045
+ const directory = isDirectory2 ? target : path6.dirname(target);
3046
+ if (fs6.existsSync(directory)) {
2623
3047
  enqueue(directory, file, reference, anchored && !open);
2624
3048
  }
2625
3049
  }
2626
3050
  };
2627
3051
  const reached = /* @__PURE__ */ new Set();
2628
3052
  const reach = (file) => {
2629
- if (reached.has(file) || DOCUMENTS.includes(path5.extname(file).toLowerCase())) {
3053
+ if (reached.has(file) || DOCUMENTS.includes(path6.extname(file).toLowerCase())) {
2630
3054
  return;
2631
3055
  }
2632
3056
  reached.add(file);
2633
3057
  let text;
2634
3058
  try {
2635
- const content = fs5.readFileSync(file);
3059
+ const content = fs6.readFileSync(file);
2636
3060
  if (content.length > SCAN_LIMIT || content.subarray(0, 8e3).includes(0)) {
2637
3061
  return;
2638
3062
  }
@@ -2664,7 +3088,7 @@ function snapshotNow(input) {
2664
3088
  const end = (match.index ?? 0) + match[0].length;
2665
3089
  const open = match[3] === void 0 || /^\s*[,+.}[]/.test(text.slice(end, end + 200));
2666
3090
  const reference = match[0].slice(0, match[0].length - (match[3]?.length ?? 0));
2667
- follow(path5.posix.join(...segments), open, true, file, reference);
3091
+ follow(path6.posix.join(...segments), open, true, file, reference);
2668
3092
  }
2669
3093
  for (const match of text.matchAll(UPWARD)) {
2670
3094
  const at = match.index ?? 0;
@@ -2684,35 +3108,35 @@ function snapshotNow(input) {
2684
3108
  }
2685
3109
  };
2686
3110
  const add = (relative) => {
2687
- const file = path5.join(listRoot, relative);
3111
+ const file = path6.join(listRoot, relative);
2688
3112
  if (relative in files || written.some((target) => isInside(target, file))) {
2689
3113
  return;
2690
3114
  }
2691
3115
  let stat;
2692
3116
  try {
2693
- stat = fs5.lstatSync(file);
3117
+ stat = fs6.lstatSync(file);
2694
3118
  } catch {
2695
3119
  files[relative] = "missing";
2696
3120
  return;
2697
3121
  }
2698
3122
  if (stat.isSymbolicLink()) {
2699
- files[relative] = `link ${fs5.readlinkSync(file)}`;
3123
+ files[relative] = `link ${fs6.readlinkSync(file)}`;
2700
3124
  let target;
2701
3125
  try {
2702
- target = fs5.realpathSync(file);
3126
+ target = fs6.realpathSync(file);
2703
3127
  } catch {
2704
3128
  return;
2705
3129
  }
2706
- enqueue(fs5.statSync(target).isDirectory() ? target : path5.dirname(target), file);
3130
+ enqueue(fs6.statSync(target).isDirectory() ? target : path6.dirname(target), file);
2707
3131
  } else if (stat.isDirectory()) {
2708
3132
  files[relative] = "directory";
2709
3133
  walk(file, true).forEach(add);
2710
3134
  } else if (!stat.isFile()) {
2711
3135
  files[relative] = "special";
2712
3136
  } else {
2713
- const content = fs5.readFileSync(file);
3137
+ const content = fs6.readFileSync(file);
2714
3138
  files[relative] = hashOf(content);
2715
- const document = DOCUMENTS.includes(path5.extname(file).toLowerCase());
3139
+ const document = DOCUMENTS.includes(path6.extname(file).toLowerCase());
2716
3140
  const loaded = followed === null || !isInside(dir, file) || followed.has(file);
2717
3141
  if (loaded && !document && content.length <= SCAN_LIMIT && !content.subarray(0, 8e3).includes(0)) {
2718
3142
  scan(content.toString("utf8").replace(/\\[\\/]/g, "/"), file);
@@ -2723,32 +3147,32 @@ function snapshotNow(input) {
2723
3147
  const besideAncestors = /* @__PURE__ */ new Set();
2724
3148
  const ancestorFiles = ancestors.flatMap((directory) => list(directory, false));
2725
3149
  for (const relative of ancestorFiles) {
2726
- besideAncestors.add(path5.join(listRoot, relative));
3150
+ besideAncestors.add(path6.join(listRoot, relative));
2727
3151
  }
2728
3152
  ancestorFiles.forEach(add);
2729
3153
  for (const directory of [dir, ...ancestors]) {
2730
- const patches = path5.join(directory, "patches");
2731
- if (fs5.existsSync(patches) && fs5.statSync(patches).isDirectory()) {
3154
+ const patches = path6.join(directory, "patches");
3155
+ if (fs6.existsSync(patches) && fs6.statSync(patches).isDirectory()) {
2732
3156
  enqueue(patches, directory);
2733
3157
  }
2734
3158
  }
2735
3159
  for (const directory of [dir, ...ancestors]) {
2736
- for (const linked of linkedPackages(path5.join(directory, "node_modules"), listRoot)) {
3160
+ for (const linked of linkedPackages(path6.join(directory, "node_modules"), listRoot)) {
2737
3161
  if (!isInside(linked, dir)) {
2738
- enqueue(linked, path5.join(directory, "node_modules"));
3162
+ enqueue(linked, path6.join(directory, "node_modules"));
2739
3163
  }
2740
3164
  }
2741
3165
  }
2742
3166
  const above = withinBounds(listRoot).slice(1);
2743
3167
  for (const directory of [dir, ...ancestors, ...above]) {
2744
3168
  const candidates = [
2745
- ...INSTALL_MARKERS.map((marker) => path5.join(directory, "node_modules", marker)),
2746
- ...above.includes(directory) ? LOCK_FILES.map((name) => path5.join(directory, name)) : []
3169
+ ...INSTALL_MARKERS.map((marker) => path6.join(directory, "node_modules", marker)),
3170
+ ...above.includes(directory) ? LOCK_FILES.map((name) => path6.join(directory, name)) : []
2747
3171
  ];
2748
3172
  for (const file of candidates) {
2749
3173
  const hash = hashFile(file);
2750
3174
  if (hash !== null) {
2751
- files[toPosix(path5.relative(listRoot, file))] = hash;
3175
+ files[toPosix(path6.relative(listRoot, file))] = hash;
2752
3176
  }
2753
3177
  }
2754
3178
  }
@@ -2760,9 +3184,9 @@ function snapshotNow(input) {
2760
3184
  };
2761
3185
  drain();
2762
3186
  for (const file of followed ?? []) {
2763
- const parts = path5.relative(listRoot, file).split(path5.sep);
2764
- if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) && fs5.statSync(file, { throwIfNoEntry: false })?.isFile() === true) {
2765
- add(toPosix(path5.relative(listRoot, file)));
3187
+ const parts = path6.relative(listRoot, file).split(path6.sep);
3188
+ if (isInside(listRoot, file) && !parts.some((part) => NEVER.includes(part)) && fs6.statSync(file, { throwIfNoEntry: false })?.isFile() === true) {
3189
+ add(toPosix(path6.relative(listRoot, file)));
2766
3190
  }
2767
3191
  }
2768
3192
  drain();
@@ -2782,14 +3206,18 @@ function snapshotNow(input) {
2782
3206
  }
2783
3207
 
2784
3208
  // src/affected-files.ts
2785
- import fs6 from "fs";
3209
+ import fs7 from "fs";
2786
3210
  import { builtinModules as builtinModules2 } from "module";
2787
- import path6 from "path";
3211
+ import path7 from "path";
2788
3212
  var AffectedError = class extends Error {
2789
3213
  };
2790
- var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readdirSync)\b|\bimport\.meta\.(?:glob|resolve)\b/;
3214
+ var RUNTIME_PATHS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire|toMatchFileSnapshot|readFileSync|readFile|readdirSync|fromFile|fromURL)\b|\bimport\.meta\.(?:glob|resolve)\b/;
3215
+ var PATH_FROM_TOP = /^[^\s./?#*:][^\s?#*:]*\.(?:html?|md|json)$/i;
3216
+ var withoutQuery = (specifier) => specifier.replace(/\?[^/]*$/, "");
3217
+ var RUNTIME_LOADS = /\b(?:require\.(?:context|resolve)|process\.cwd|createRequire)\b|\bimport\.meta\.(?:glob|resolve)\b/;
2791
3218
  var LOCATION = /\b(?:__dirname|__filename)\b|\bimport\.meta\.(?:url|dirname|filename)\b/;
2792
3219
  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\(/;
3220
+ var LOCATION_LOADS = /\b(?:requireActual|requireMock|importActual|importMock|mock|doMock|unstable_mockModule|setMock|createMockFromModule)\(\s*(?![\s'"`)])|\bWorker\(/;
2793
3221
  var LOCATION_SAFE = /* @__PURE__ */ new Set([
2794
3222
  "path",
2795
3223
  "path/posix",
@@ -2810,30 +3238,31 @@ var FILE_MODULES = /* @__PURE__ */ new Set([
2810
3238
  "fast-glob",
2811
3239
  "globby"
2812
3240
  ]);
3241
+ var LOADING_MODULES = /* @__PURE__ */ new Set(["child_process", "module", "worker_threads"]);
2813
3242
  var bare = (specifier) => specifier.replace(/^node:/, "");
2814
3243
  function linkedPackage(specifier, from) {
2815
3244
  const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
2816
- for (let current = path6.dirname(from); ; current = path6.dirname(current)) {
2817
- const candidate = path6.join(current, "node_modules", name);
2818
- if (fs6.existsSync(candidate)) {
3245
+ for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
3246
+ const candidate = path7.join(current, "node_modules", name);
3247
+ if (fs7.existsSync(candidate)) {
2819
3248
  const target = realpath(candidate);
2820
- return target.split(path6.sep).includes("node_modules") ? null : target;
3249
+ return target.split(path7.sep).includes("node_modules") ? null : target;
2821
3250
  }
2822
- if (current === path6.dirname(current)) {
3251
+ if (current === path7.dirname(current)) {
2823
3252
  return null;
2824
3253
  }
2825
3254
  }
2826
3255
  }
2827
- var toPosix2 = (file) => file.split(path6.sep).join("/");
3256
+ var toPosix2 = (file) => file.split(path7.sep).join("/");
2828
3257
  function realpath(file) {
2829
3258
  try {
2830
- return fs6.realpathSync.native(file);
3259
+ return fs7.realpathSync.native(file);
2831
3260
  } catch {
2832
3261
  return file;
2833
3262
  }
2834
3263
  }
2835
3264
  function runnerConfigs(dir, stryker, options) {
2836
- const files = [path6.resolve(dir, stryker)];
3265
+ const files = [path7.resolve(dir, stryker)];
2837
3266
  const named = options.testRunner === "vitest" ? options.vitestConfig : options.jestConfig;
2838
3267
  if (options.testRunner !== "vitest" && options.testRunner !== "jest") {
2839
3268
  throw new AffectedError(
@@ -2841,20 +3270,20 @@ function runnerConfigs(dir, stryker, options) {
2841
3270
  );
2842
3271
  }
2843
3272
  if (typeof named === "string") {
2844
- files.push(path6.resolve(dir, named));
3273
+ files.push(path7.resolve(dir, named));
2845
3274
  } else {
2846
3275
  const names = options.testRunner === "vitest" ? ["vitest.config", "vite.config"] : ["jest.config"];
2847
3276
  for (const name of names) {
2848
3277
  for (const extension of [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"]) {
2849
- const file = path6.join(dir, name + extension);
2850
- if (fs6.existsSync(file)) {
3278
+ const file = path7.join(dir, name + extension);
3279
+ if (fs7.existsSync(file)) {
2851
3280
  files.push(file);
2852
3281
  }
2853
3282
  }
2854
3283
  }
2855
3284
  }
2856
3285
  return {
2857
- files: files.filter((file) => fs6.existsSync(file)).map(realpath),
3286
+ files: files.filter((file) => fs7.existsSync(file)).map(realpath),
2858
3287
  runner: options.testRunner,
2859
3288
  checkers: Array.isArray(options.checkers) && options.checkers.length > 0
2860
3289
  };
@@ -2875,24 +3304,24 @@ function typesErased(configs, texts, compiler) {
2875
3304
  erased = /\bts-jest\b/.test(all) ? TS_JEST_STRIPS.test(all) && !TS_JEST_UNSURE.test(all) : STRIPPING_TRANSFORMS.test(all) && !/\bpreset\b/.test(all);
2876
3305
  }
2877
3306
  }
2878
- const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(path6.basename(file))) && compiler.every(({ text }) => text !== null && !KEEPS_IMPORTS.test(text));
3307
+ const inline = erased && !KEEPS_IMPORTS.test(all) && compiler.some(({ file }) => /^tsconfig/.test(path7.basename(file))) && compiler.every(({ text }) => text !== null && !KEEPS_IMPORTS.test(text));
2879
3308
  return { erased, inline };
2880
3309
  }
2881
3310
  function compilerConfigs(dir, root, keys) {
2882
3311
  const above = /* @__PURE__ */ new Set();
2883
- for (let current = path6.dirname(dir); ; current = path6.dirname(current)) {
2884
- const relative = path6.relative(root, current);
2885
- if (relative.startsWith("..") || path6.isAbsolute(relative)) {
3312
+ for (let current = path7.dirname(dir); ; current = path7.dirname(current)) {
3313
+ const relative = path7.relative(root, current);
3314
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) {
2886
3315
  break;
2887
3316
  }
2888
3317
  above.add(current);
2889
- if (current === path6.dirname(current)) {
3318
+ if (current === path7.dirname(current)) {
2890
3319
  break;
2891
3320
  }
2892
3321
  }
2893
- const compared = new Set(keys.map((key) => path6.join(root, key)));
3322
+ const compared = new Set(keys.map((key) => path7.join(root, key)));
2894
3323
  const starts = [...compared].filter(
2895
- (file) => (COMPILER_CONFIG.test(path6.basename(file)) || path6.basename(file) === "package.json") && (file.startsWith(`${dir}${path6.sep}`) || above.has(path6.dirname(file)))
3324
+ (file) => (COMPILER_CONFIG.test(path7.basename(file)) || path7.basename(file) === "package.json") && (file.startsWith(`${dir}${path7.sep}`) || above.has(path7.dirname(file)))
2896
3325
  );
2897
3326
  const found = [];
2898
3327
  const seen = /* @__PURE__ */ new Set();
@@ -2903,9 +3332,9 @@ function compilerConfigs(dir, root, keys) {
2903
3332
  }
2904
3333
  seen.add(file);
2905
3334
  let text = null;
2906
- if (compared.has(file) || file.split(path6.sep).includes("node_modules")) {
3335
+ if (compared.has(file) || file.split(path7.sep).includes("node_modules")) {
2907
3336
  try {
2908
- text = fs6.readFileSync(file, "utf8");
3337
+ text = fs7.readFileSync(file, "utf8");
2909
3338
  } catch {
2910
3339
  text = null;
2911
3340
  }
@@ -2921,24 +3350,24 @@ function extended(base, from) {
2921
3350
  const candidates = (start) => [
2922
3351
  start,
2923
3352
  `${start}.json`,
2924
- path6.join(start, "tsconfig.json")
3353
+ path7.join(start, "tsconfig.json")
2925
3354
  ];
2926
- if (base.startsWith(".") || path6.isAbsolute(base)) {
2927
- return candidates(path6.resolve(path6.dirname(from), base)).find(isFile2) ?? null;
3355
+ if (base.startsWith(".") || path7.isAbsolute(base)) {
3356
+ return candidates(path7.resolve(path7.dirname(from), base)).find(isFile2) ?? null;
2928
3357
  }
2929
- for (let current = path6.dirname(from); ; current = path6.dirname(current)) {
2930
- const file = candidates(path6.join(current, "node_modules", base)).find(isFile2);
3358
+ for (let current = path7.dirname(from); ; current = path7.dirname(current)) {
3359
+ const file = candidates(path7.join(current, "node_modules", base)).find(isFile2);
2931
3360
  if (file !== void 0) {
2932
3361
  return file;
2933
3362
  }
2934
- if (current === path6.dirname(current)) {
3363
+ if (current === path7.dirname(current)) {
2935
3364
  return null;
2936
3365
  }
2937
3366
  }
2938
3367
  }
2939
3368
  function isFile2(file) {
2940
3369
  try {
2941
- return fs6.statSync(file).isFile();
3370
+ return fs7.statSync(file).isFile();
2942
3371
  } catch {
2943
3372
  return false;
2944
3373
  }
@@ -2949,29 +3378,33 @@ function fileEdges(file, context) {
2949
3378
  if (known !== void 0) {
2950
3379
  return known;
2951
3380
  }
2952
- const shown = (one) => toPosix2(path6.relative(dir, one));
3381
+ const shown = (one) => toPosix2(path7.relative(dir, one));
2953
3382
  const target = (specifier, from) => {
2954
3383
  if (specifier.startsWith(".")) {
2955
- return resolveFiles(path6.resolve(path6.dirname(from), specifier));
3384
+ return resolveFiles(path7.resolve(path7.dirname(from), specifier));
2956
3385
  }
2957
3386
  const alias = aliases.find((candidate) => specifier.startsWith(candidate.prefix));
2958
- return alias === void 0 ? [] : resolveFiles(path6.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
3387
+ return alias === void 0 ? [] : resolveFiles(path7.resolve(dir, alias.target + specifier.slice(alias.prefix.length)));
3388
+ };
3389
+ const found = { loads: [], unknown: null, loading: null, computed: false, unnamed: [] };
3390
+ const loading = (reason) => {
3391
+ found.unknown ??= reason;
3392
+ found.loading ??= reason;
2959
3393
  };
2960
- const found = { loads: [], unknown: null, computed: false, unnamed: [] };
2961
3394
  cache.set(file, found);
2962
- const snapshotFile = path6.join(
2963
- path6.dirname(file),
3395
+ const snapshotFile = path7.join(
3396
+ path7.dirname(file),
2964
3397
  "__snapshots__",
2965
- `${path6.basename(file)}.snap`
3398
+ `${path7.basename(file)}.snap`
2966
3399
  );
2967
- if (fs6.existsSync(snapshotFile)) {
3400
+ if (fs7.existsSync(snapshotFile)) {
2968
3401
  found.loads.push(realpath(snapshotFile));
2969
3402
  }
2970
- const mocks = path6.join(path6.dirname(file), "__mocks__");
2971
- const name = path6.basename(file);
3403
+ const mocks = path7.join(path7.dirname(file), "__mocks__");
3404
+ const name = path7.basename(file);
2972
3405
  const candidates = isDirectory(mocks) ? [
2973
- path6.join(mocks, name),
2974
- ...resolveFiles(path6.join(mocks, name.replace(/\.[^.]+$/, "")), false, false)
3406
+ path7.join(mocks, name),
3407
+ ...resolveFiles(path7.join(mocks, name.replace(/\.[^.]+$/, "")), false, false)
2975
3408
  ] : [];
2976
3409
  for (const mock of new Set(candidates)) {
2977
3410
  if (isFile2(mock)) {
@@ -2981,18 +3414,23 @@ function fileEdges(file, context) {
2981
3414
  if (!SCRIPT.test(file) && !COMPONENT.test(file)) {
2982
3415
  return found;
2983
3416
  }
2984
- const source = fs6.readFileSync(file, "utf8");
3417
+ const source = fs7.readFileSync(file, "utf8");
2985
3418
  const code = withoutComments(source);
2986
3419
  if (COMPONENT.test(file)) {
2987
- found.unknown = `${shown(file)} is a component, which can use others that it does not import`;
3420
+ loading(`${shown(file)} is a component, which can use others that it does not import`);
2988
3421
  }
2989
3422
  const uncheckable = findUncheckableImports(code)[0];
2990
3423
  const runtime = RUNTIME_PATHS.exec(code);
3424
+ const runtimeLoads = RUNTIME_LOADS.exec(code);
2991
3425
  if (uncheckable !== void 0) {
2992
3426
  found.unknown = `${shown(file)}:${uncheckable} imports a module whose path is not written out`;
3427
+ found.loading = found.unknown;
2993
3428
  found.computed = true;
2994
3429
  } else if (runtime !== null) {
2995
3430
  found.unknown = `${shown(file)} uses ${runtime[0]}, which can reach files that no import names`;
3431
+ if (runtimeLoads !== null) {
3432
+ found.loading ??= `${shown(file)} uses ${runtimeLoads[0]}, which can reach files that no import names`;
3433
+ }
2996
3434
  }
2997
3435
  const location = LOCATION.exec(code);
2998
3436
  const refs = parseImports(code);
@@ -3007,30 +3445,39 @@ function fileEdges(file, context) {
3007
3445
  typeLines.set(key, (typeLines.get(key) ?? 0) + 1);
3008
3446
  }
3009
3447
  for (const specifier of specifiers) {
3010
- if (path6.isAbsolute(specifier) || specifier.startsWith("file:")) {
3448
+ if (path7.isAbsolute(specifier) || specifier.startsWith("file:")) {
3011
3449
  const resolved2 = resolveFiles(
3012
- specifier.startsWith("file:") ? new URL(specifier).pathname : specifier
3450
+ withoutQuery(specifier.startsWith("file:") ? new URL(specifier).pathname : specifier)
3013
3451
  );
3014
3452
  if (resolved2.length === 0) {
3015
- found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
3453
+ loading(`${shown(file)} imports ${specifier}, which leads to no file`);
3016
3454
  } else {
3017
3455
  found.loads.push(...resolved2.map(realpath));
3018
3456
  }
3019
3457
  } else if (isProjectImport(specifier, aliases)) {
3020
- const resolved2 = target(specifier, file);
3458
+ const resolved2 = target(withoutQuery(specifier), file);
3021
3459
  if (resolved2.length === 0) {
3022
- found.unknown ??= `${shown(file)} imports ${specifier}, which leads to no file`;
3460
+ loading(`${shown(file)} imports ${specifier}, which leads to no file`);
3023
3461
  } else {
3024
3462
  found.loads.push(...resolved2.map(realpath));
3025
3463
  }
3026
3464
  } else if (FILE_MODULES.has(bare(specifier))) {
3027
- found.unknown ??= `${shown(file)} imports ${specifier}, which can reach files that no import names`;
3465
+ const reason = `${shown(file)} imports ${specifier}, which can reach files that no import names`;
3466
+ if (LOADING_MODULES.has(bare(specifier))) {
3467
+ loading(reason);
3468
+ } else {
3469
+ found.unknown ??= reason;
3470
+ }
3028
3471
  } else if (!specifier.startsWith("node:") && !builtinModules2.includes(specifier)) {
3029
3472
  if (!installed(specifier, file)) {
3030
- found.unknown ??= `${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`;
3473
+ loading(
3474
+ `${shown(file)} imports ${specifier}, which is neither a file nor an installed package (name an alias in settings.importAliases)`
3475
+ );
3031
3476
  found.unnamed.push(specifier);
3032
3477
  } else if (linkedPackage(specifier, file) !== null) {
3033
- found.unknown ??= `${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`;
3478
+ loading(
3479
+ `${shown(file)} imports ${specifier}, a package of the repository linked into node_modules`
3480
+ );
3034
3481
  found.computed = true;
3035
3482
  }
3036
3483
  }
@@ -3038,10 +3485,14 @@ function fileEdges(file, context) {
3038
3485
  if (location !== null) {
3039
3486
  const reads = LOCATION_READS.exec(code);
3040
3487
  const reader = [...specifiers].find(
3041
- (specifier) => !specifier.startsWith(".") && !path6.isAbsolute(specifier) && !specifier.startsWith("file:") && !isProjectImport(specifier, aliases) && !LOCATION_SAFE.has(bare(specifier))
3488
+ (specifier) => !specifier.startsWith(".") && !path7.isAbsolute(specifier) && !specifier.startsWith("file:") && !isProjectImport(specifier, aliases) && !LOCATION_SAFE.has(bare(specifier))
3042
3489
  );
3043
3490
  if (reads !== null) {
3044
3491
  found.unknown ??= `${shown(file)} uses ${location[0]} and ${reads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
3492
+ const loads = LOCATION_LOADS.exec(code);
3493
+ if (loads !== null) {
3494
+ found.loading ??= `${shown(file)} uses ${location[0]} and ${loads[0].replace(/\(\s*$/, "")}, which can reach files that no import names`;
3495
+ }
3045
3496
  } else if (reader !== void 0) {
3046
3497
  found.unknown ??= `${shown(file)} uses ${location[0]} and imports ${reader}, which can read files by it that no import names`;
3047
3498
  }
@@ -3060,7 +3511,9 @@ function fileEdges(file, context) {
3060
3511
  if (left > 0) {
3061
3512
  typeLines.set(key, left - 1);
3062
3513
  } else if (text.startsWith(".") && !specifiers.has(text)) {
3063
- found.loads.push(...resolveFiles(path6.resolve(path6.dirname(file), text)).map(realpath));
3514
+ found.loads.push(...resolveFiles(path7.resolve(path7.dirname(file), text)).map(realpath));
3515
+ } else if (PATH_FROM_TOP.test(text) && isFile2(path7.resolve(dir, text))) {
3516
+ found.loads.push(realpath(path7.resolve(dir, text)));
3064
3517
  }
3065
3518
  }
3066
3519
  return found;
@@ -3072,25 +3525,25 @@ function inputsOf(manifest, input) {
3072
3525
  const dir = realpath(manifest.dir);
3073
3526
  const aliases = manifest.settings.importAliases;
3074
3527
  const placed = [...input.tests, ...Object.values(input.covering).flat()];
3075
- const absolute = placed.find((file) => path6.isAbsolute(file) || file.startsWith(".."));
3528
+ const absolute = placed.find((file) => path7.isAbsolute(file) || file.startsWith(".."));
3076
3529
  if (absolute !== void 0) {
3077
3530
  throw new AffectedError(`the report names a spec file outside of the subproject: ${absolute}`);
3078
3531
  }
3079
3532
  const keyOf2 = (file) => {
3080
- const relative = path6.relative(input.root, realpath(file));
3081
- if (relative.startsWith("..") || path6.isAbsolute(relative)) {
3533
+ const relative = path7.relative(input.root, realpath(file));
3534
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) {
3082
3535
  throw new AffectedError(`${file} is outside of the files that are compared`);
3083
3536
  }
3084
3537
  return toPosix2(relative);
3085
3538
  };
3086
- const shown = (file) => toPosix2(path6.relative(dir, file));
3539
+ const shown = (file) => toPosix2(path7.relative(dir, file));
3087
3540
  const plain2 = /* @__PURE__ */ new Map();
3088
3541
  const configTexts = [];
3089
3542
  const runnerSources = [];
3090
3543
  const reached = new Set(input.configs.files);
3091
3544
  const pending = [...input.configs.files];
3092
3545
  for (let file = pending.shift(); file !== void 0; file = pending.shift()) {
3093
- const text = fs6.readFileSync(file, "utf8");
3546
+ const text = fs7.readFileSync(file, "utf8");
3094
3547
  configTexts.push(text);
3095
3548
  runnerSources.push({ file, text });
3096
3549
  for (const next of fileEdges(file, { dir, aliases, cache: plain2 }).loads) {
@@ -3108,7 +3561,7 @@ function inputsOf(manifest, input) {
3108
3561
  // Not what lies outside of the compared files: this package, imported by path, is installed.
3109
3562
  runner: [
3110
3563
  ...runnerSources.filter(({ file }) => {
3111
- const relative = path6.relative(input.root, realpath(file));
3564
+ const relative = path7.relative(input.root, realpath(file));
3112
3565
  return !relative.startsWith("..") && toPosix2(relative) in input.files;
3113
3566
  }),
3114
3567
  ...jestInPackage(dir, input.configs)
@@ -3121,14 +3574,21 @@ function inputsOf(manifest, input) {
3121
3574
  }
3122
3575
  const edges = /* @__PURE__ */ new Map();
3123
3576
  const edgesOf = (file) => fileEdges(file, { dir, aliases, cache: edges, types });
3577
+ const blind = /* @__PURE__ */ Object.create(null);
3124
3578
  const closure = (start, strict) => {
3125
3579
  const seen = /* @__PURE__ */ new Set([start]);
3126
3580
  const queue = [start];
3127
3581
  for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
3128
3582
  const found = edgesOf(file);
3129
- if (strict && found.unknown !== null) {
3583
+ if (strict === "every" && found.unknown !== null) {
3130
3584
  throw new AffectedError(found.unknown);
3131
3585
  }
3586
+ if (strict === "gate" && found.unknown !== null) {
3587
+ if (found.loading !== null) {
3588
+ throw new AffectedError(found.loading);
3589
+ }
3590
+ blind[keyOf2(file)] = found.unknown;
3591
+ }
3132
3592
  for (const next of found.loads) {
3133
3593
  if (!seen.has(next)) {
3134
3594
  seen.add(next);
@@ -3139,8 +3599,8 @@ function inputsOf(manifest, input) {
3139
3599
  return [...seen];
3140
3600
  };
3141
3601
  const isCompared = (file) => {
3142
- const relative = path6.relative(input.root, realpath(file));
3143
- return !relative.startsWith("..") && !path6.isAbsolute(relative) && toPosix2(relative) in input.files;
3602
+ const relative = path7.relative(input.root, realpath(file));
3603
+ return !relative.startsWith("..") && !path7.isAbsolute(relative) && toPosix2(relative) in input.files;
3144
3604
  };
3145
3605
  const keysOf = (files) => files.map((file) => {
3146
3606
  const key = keyOf2(file);
@@ -3159,10 +3619,10 @@ function inputsOf(manifest, input) {
3159
3619
  if (!SCRIPT.test(loaded)) {
3160
3620
  continue;
3161
3621
  }
3162
- for (const match of fs6.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
3622
+ for (const match of fs7.readFileSync(loaded, "utf8").matchAll(QUOTED)) {
3163
3623
  const text = match[2].replace(/^<rootDir>\//, "");
3164
- const here = resolveFiles(path6.resolve(path6.dirname(loaded), text), false, false);
3165
- const candidates = here.length > 0 ? here : resolveFiles(path6.resolve(dir, text), false, false);
3624
+ const here = resolveFiles(path7.resolve(path7.dirname(loaded), text), false, false);
3625
+ const candidates = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false, false);
3166
3626
  for (const found of candidates) {
3167
3627
  if (text.includes("*") || !isCompared(found)) {
3168
3628
  continue;
@@ -3177,7 +3637,7 @@ function inputsOf(manifest, input) {
3177
3637
  }
3178
3638
  }
3179
3639
  const gatePaths = new Map(
3180
- manifest.gates.map((gate) => [realpath(path6.join(dir, gate.path)), gate.path])
3640
+ manifest.gates.map((gate) => [realpath(path7.join(dir, gate.path)), gate.path])
3181
3641
  );
3182
3642
  const tests = [
3183
3643
  .../* @__PURE__ */ new Set([
@@ -3188,13 +3648,13 @@ function inputsOf(manifest, input) {
3188
3648
  ];
3189
3649
  const runAsSuch = new Set(
3190
3650
  [...tests, ...manifest.gates.map((gate) => gate.path)].map(
3191
- (file) => realpath(path6.join(dir, file))
3651
+ (file) => realpath(path7.join(dir, file))
3192
3652
  )
3193
3653
  );
3194
- const subprojectKey = toPosix2(path6.relative(input.root, dir));
3654
+ const subprojectKey = toPosix2(path7.relative(input.root, dir));
3195
3655
  const isConfig = (file) => input.configs.files.includes(file) || kindOf(keyOf2(file), subprojectKey) === "config";
3196
3656
  for (const file of named.filter((one) => !isConfig(one) && !runAsSuch.has(one))) {
3197
- for (const loaded of closure(file, true)) {
3657
+ for (const loaded of closure(file, "every")) {
3198
3658
  const gate = gatePaths.get(loaded);
3199
3659
  if (gate !== void 0) {
3200
3660
  throw new AffectedError(
@@ -3204,16 +3664,16 @@ function inputsOf(manifest, input) {
3204
3664
  config.add(loaded);
3205
3665
  }
3206
3666
  }
3207
- const fromSubproject = (file) => path6.join(dir, file);
3667
+ const fromSubproject = (file) => path7.join(dir, file);
3208
3668
  const closures = /* @__PURE__ */ Object.create(null);
3209
3669
  for (const file of [...manifest.gates.map((gate) => gate.path), ...tests]) {
3210
3670
  const absolute2 = fromSubproject(file);
3211
- if (!fs6.existsSync(absolute2)) {
3671
+ if (!fs7.existsSync(absolute2)) {
3212
3672
  continue;
3213
3673
  }
3214
- closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), true));
3674
+ closures[keysOf([absolute2])[0]] = keysOf(closure(realpath(absolute2), "gate"));
3215
3675
  }
3216
- const keyed = (files) => files.map((file) => toPosix2(path6.relative(input.root, path6.join(dir, file))));
3676
+ const keyed = (files) => files.map((file) => toPosix2(path7.relative(input.root, path7.join(dir, file))));
3217
3677
  const dependencies = gateDependencies({
3218
3678
  gates: manifest.gates.map((gate) => ({
3219
3679
  key: keyed([gate.path])[0],
@@ -3223,9 +3683,10 @@ function inputsOf(manifest, input) {
3223
3683
  covering: Object.fromEntries(
3224
3684
  Object.entries(input.covering).map(([gate, specs]) => [keyed([gate])[0], keyed(specs)])
3225
3685
  ),
3226
- closures
3686
+ closures,
3687
+ blind
3227
3688
  });
3228
- const subproject = toPosix2(path6.relative(input.root, dir));
3689
+ const subproject = toPosix2(path7.relative(input.root, dir));
3229
3690
  const prefix = subproject === "" ? "" : `${subproject}/`;
3230
3691
  const gates = /* @__PURE__ */ Object.create(null);
3231
3692
  for (const gate of manifest.gates) {
@@ -3236,12 +3697,13 @@ function inputsOf(manifest, input) {
3236
3697
  specs: found.specs.map(
3237
3698
  (spec) => spec.startsWith(prefix) ? spec.slice(prefix.length) : spec
3238
3699
  ),
3239
- files: found.files
3700
+ files: found.files,
3701
+ ...found.blind === void 0 ? {} : { blind: found.blind }
3240
3702
  };
3241
3703
  }
3242
3704
  const packageMocks = Object.keys(input.files).filter(
3243
3705
  (key) => isNestedMock(key, subproject) && mockNames(key).some(
3244
- (name) => builtinModules2.includes(name) || installed(name, path6.join(input.root, key))
3706
+ (name) => builtinModules2.includes(name) || installed(name, path7.join(input.root, key))
3245
3707
  )
3246
3708
  );
3247
3709
  return {
@@ -3297,7 +3759,7 @@ function scopeOf(manifest, configs, listed2) {
3297
3759
  const cache = /* @__PURE__ */ new Map();
3298
3760
  const context = { dir, aliases: manifest.settings.importAliases, cache };
3299
3761
  const runner = configs.files.slice(1);
3300
- const texts = runner.map((file) => withoutComments(fs6.readFileSync(file, "utf8")));
3762
+ const texts = runner.map((file) => withoutComments(fs7.readFileSync(file, "utf8")));
3301
3763
  if (runner.length === 0 || !texts.every((text) => OWN.test(text))) {
3302
3764
  return { every: NOT_THE_HELPER2 };
3303
3765
  }
@@ -3312,21 +3774,21 @@ function scopeOf(manifest, configs, listed2) {
3312
3774
  fileEdges(file, context).loads.forEach((loaded) => parts.add(loaded));
3313
3775
  }
3314
3776
  }
3315
- if ([...parts].some((file) => PROJECTS.test(withoutComments(fs6.readFileSync(file, "utf8"))))) {
3777
+ if ([...parts].some((file) => PROJECTS.test(withoutComments(fs7.readFileSync(file, "utf8"))))) {
3316
3778
  return {
3317
3779
  every: "the runner config, or a file it is put together from, names projects or a workspace"
3318
3780
  };
3319
3781
  }
3320
- const named = fs6.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path6.join(dir, name));
3321
- const mocks = path6.join(dir, "__mocks__");
3782
+ const named = fs7.readdirSync(dir).filter((name) => READ_BY_NAME.some((pattern) => pattern.test(name))).map((name) => path7.join(dir, name));
3783
+ const mocks = path7.join(dir, "__mocks__");
3322
3784
  const seeds = [
3323
3785
  ...configs.files,
3324
3786
  ...named,
3325
- ...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path6.join(dir, file)),
3326
- ...listed2.filter((file) => file.startsWith(`${mocks}${path6.sep}`))
3327
- ].filter((file) => fs6.existsSync(file)).map(realpath);
3787
+ ...manifest.gates.flatMap((gate) => [gate.path, ...gate.existingSpecs]).map((file) => path7.join(dir, file)),
3788
+ ...listed2.filter((file) => file.startsWith(`${mocks}${path7.sep}`))
3789
+ ].filter((file) => fs7.existsSync(file)).map(realpath);
3328
3790
  const naming = new Set(
3329
- [...configs.files, ...named].filter((file) => fs6.existsSync(file)).map(realpath)
3791
+ [...configs.files, ...named].filter((file) => fs7.existsSync(file)).map(realpath)
3330
3792
  );
3331
3793
  const seen = new Set(seeds);
3332
3794
  const queue = [...seeds];
@@ -3338,7 +3800,7 @@ function scopeOf(manifest, configs, listed2) {
3338
3800
  }
3339
3801
  };
3340
3802
  for (let file = queue.shift(); file !== void 0; file = queue.shift()) {
3341
- if (fs6.statSync(file).isDirectory()) {
3803
+ if (fs7.statSync(file).isDirectory()) {
3342
3804
  continue;
3343
3805
  }
3344
3806
  const edges = fileEdges(file, context);
@@ -3349,11 +3811,11 @@ function scopeOf(manifest, configs, listed2) {
3349
3811
  for (const specifier of edges.unnamed) {
3350
3812
  endingAs(specifier, listed2).forEach(add);
3351
3813
  }
3352
- if (naming.has(file) || SCRIPT.test(file) && path6.dirname(file) === dir) {
3814
+ if (naming.has(file) || SCRIPT.test(file) && path7.dirname(file) === dir) {
3353
3815
  for (const named2 of namedIn(file)) {
3354
3816
  const text = named2.replace(/^<rootDir>\//, "");
3355
- const here = resolveFiles(path6.resolve(path6.dirname(file), text), false);
3356
- const found = here.length > 0 ? here : resolveFiles(path6.resolve(dir, text), false);
3817
+ const here = resolveFiles(path7.resolve(path7.dirname(file), text), false);
3818
+ const found = here.length > 0 ? here : resolveFiles(path7.resolve(dir, text), false);
3357
3819
  for (const one of text.includes("*") ? [] : found) {
3358
3820
  if (!SCRIPT.test(one)) {
3359
3821
  naming.add(realpath(one));
@@ -3377,7 +3839,7 @@ function endingAs(specifier, files) {
3377
3839
  });
3378
3840
  }
3379
3841
  function namedIn(file) {
3380
- const text = fs6.readFileSync(file, "utf8");
3842
+ const text = fs7.readFileSync(file, "utf8");
3381
3843
  if (SCRIPT.test(file)) {
3382
3844
  return [...text.matchAll(QUOTED)].map((match) => match[2]);
3383
3845
  }
@@ -3531,15 +3993,15 @@ function mayStartPattern(before) {
3531
3993
  return before.last === "" || !")]".includes(before.last);
3532
3994
  }
3533
3995
  function jestInPackage(dir, configs) {
3534
- const file = path6.join(dir, "package.json");
3535
- return configs.runner === "jest" && configs.files.length === 1 && fs6.existsSync(file) ? [{ file, text: fs6.readFileSync(file, "utf8"), mapperOnly: true }] : [];
3996
+ const file = path7.join(dir, "package.json");
3997
+ return configs.runner === "jest" && configs.files.length === 1 && fs7.existsSync(file) ? [{ file, text: fs7.readFileSync(file, "utf8"), mapperOnly: true }] : [];
3536
3998
  }
3537
3999
  var STRING_LITERAL = String.raw`'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"`;
3538
4000
  function stringValue(text) {
3539
4001
  const match = new RegExp(`^\\s*(${STRING_LITERAL})\\s*$`).exec(text);
3540
4002
  return match === null ? null : match[1].slice(1, -1).replace(/\\(.)/g, "$1");
3541
4003
  }
3542
- function partsOf(text, at) {
4004
+ function partsOf2(text, at) {
3543
4005
  const parts = [];
3544
4006
  let depth = 0;
3545
4007
  let start = at + 1;
@@ -3576,7 +4038,7 @@ function property(part) {
3576
4038
  }
3577
4039
  function aliasPath(expression, file, text, depth = 0) {
3578
4040
  const value = expression.trim();
3579
- const here = path6.dirname(file);
4041
+ const here = path7.dirname(file);
3580
4042
  if (value === "__dirname" || value === "import.meta.dirname" || /^(?:path\.)?dirname\(\s*(?:url\.)?fileURLToPath\(\s*import\.meta\.url\s*\)\s*\)$/.test(value)) {
3581
4043
  return here;
3582
4044
  }
@@ -3584,18 +4046,18 @@ function aliasPath(expression, file, text, depth = 0) {
3584
4046
  `^(?:(?: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)$`
3585
4047
  ).exec(value);
3586
4048
  if (url !== null) {
3587
- return path6.resolve(here, stringValue(url[1] ?? url[2]));
4049
+ return path7.resolve(here, stringValue(url[1] ?? url[2]));
3588
4050
  }
3589
4051
  const call = /^(?:path\.)?(?:resolve|join)\s*\(/.exec(value);
3590
4052
  if (call !== null) {
3591
- const found = partsOf(value, call[0].length - 1);
4053
+ const found = partsOf2(value, call[0].length - 1);
3592
4054
  if (found === null || value.slice(found.end + 1).trim() !== "" || found.parts.length === 0) {
3593
4055
  return null;
3594
4056
  }
3595
4057
  const [first, ...rest] = found.parts;
3596
4058
  const base = aliasPath(first, file, text, depth + 1);
3597
4059
  const parts = rest.map(stringValue);
3598
- return base === null || parts.some((part) => part === null) ? null : path6.resolve(base, ...parts);
4060
+ return base === null || parts.some((part) => part === null) ? null : path7.resolve(base, ...parts);
3599
4061
  }
3600
4062
  if (/^[A-Za-z_$][\w$]*$/.test(value) && depth < 4) {
3601
4063
  const declarations = text.match(
@@ -3611,7 +4073,7 @@ function aliasPath(expression, file, text, depth = 0) {
3611
4073
  var PACKAGE_NAME = /^(?:@[\w-][\w.-]*\/)?[\w-][\w.-]*$/;
3612
4074
  function isDirectory(file) {
3613
4075
  try {
3614
- return fs6.statSync(file).isDirectory();
4076
+ return fs7.statSync(file).isDirectory();
3615
4077
  } catch {
3616
4078
  return false;
3617
4079
  }
@@ -3637,8 +4099,8 @@ function unexplainedMaps(input) {
3637
4099
  const { dir, aliases } = input;
3638
4100
  const reasons = [];
3639
4101
  const mapped = [];
3640
- const shown = (file) => toPosix2(path6.relative(dir, file));
3641
- const under = (base, rest) => rest === "" ? `${base}/` : path6.join(base, rest);
4102
+ const shown = (file) => toPosix2(path7.relative(dir, file));
4103
+ const under = (base, rest) => rest === "" ? `${base}/` : path7.join(base, rest);
3642
4104
  const named = (prefix, target) => {
3643
4105
  const alias = aliases.find((candidate) => prefix.startsWith(candidate.prefix));
3644
4106
  if (alias === void 0) {
@@ -3646,10 +4108,10 @@ function unexplainedMaps(input) {
3646
4108
  }
3647
4109
  const full = alias.target + prefix.slice(alias.prefix.length);
3648
4110
  const slash = full === "" || full.endsWith("/");
3649
- return path6.resolve(dir, full) === path6.resolve(target) && slash === target.endsWith("/");
4111
+ return path7.resolve(dir, full) === path7.resolve(target) && slash === target.endsWith("/");
3650
4112
  };
3651
4113
  const advice = (prefix, target) => {
3652
- const relative = toPosix2(path6.relative(dir, target));
4114
+ const relative = toPosix2(path7.relative(dir, target));
3653
4115
  const shownTarget = relative === "" ? "./" : `${relative}/`;
3654
4116
  const shorter = aliases.find((alias) => prefix.startsWith(alias.prefix));
3655
4117
  const before = shorter === void 0 ? "" : shorter.prefix === prefix ? `, in place of the alias of ${prefix}` : `, before the alias of ${shorter.prefix}`;
@@ -3662,7 +4124,7 @@ function unexplainedMaps(input) {
3662
4124
  let prefix = null;
3663
4125
  let to = null;
3664
4126
  if (list) {
3665
- const object = /^\s*\{/.test(part) ? partsOf(part, part.indexOf("{")) : null;
4127
+ const object = /^\s*\{/.test(part) ? partsOf2(part, part.indexOf("{")) : null;
3666
4128
  const fields = Object.fromEntries(
3667
4129
  (object?.parts ?? []).map(property).map((one) => [one?.key ?? "", one?.value ?? ""])
3668
4130
  );
@@ -3714,7 +4176,7 @@ function unexplainedMaps(input) {
3714
4176
  const word = match[0];
3715
4177
  const opening = /^['"]?\s*:\s*/.exec(text.slice(match.index + word.length));
3716
4178
  const at = opening === null ? -1 : match.index + word.length + opening[0].length;
3717
- const found = at === -1 ? null : partsOf(text, at);
4179
+ const found = at === -1 ? null : partsOf2(text, at);
3718
4180
  const unreadable = `${shown(file)} names ${word} in a way that cannot be read as a list of aliases`;
3719
4181
  if (found === null || text[at] !== "{" && !(word === "alias" && text[at] === "[")) {
3720
4182
  reasons.push(unreadable);
@@ -3744,7 +4206,7 @@ function unexplainedMaps(input) {
3744
4206
  }
3745
4207
  const prefix = mapperPrefix(entry.key);
3746
4208
  const target = value === null ? null : /^<rootDir>\/(.*)\$1$/.exec(value);
3747
- const to = target === null ? null : under(path6.dirname(file), target[1]);
4209
+ const to = target === null ? null : under(path7.dirname(file), target[1]);
3748
4210
  if (prefix === null || to === null) {
3749
4211
  reasons.push(missing(file, entry.key, word));
3750
4212
  } else if (!named(prefix, to)) {
@@ -3755,19 +4217,19 @@ function unexplainedMaps(input) {
3755
4217
  }
3756
4218
  }
3757
4219
  }
3758
- const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(path6.basename(file)));
4220
+ const tsconfigs = input.compiler.filter(({ file }) => /^[jt]sconfig/.test(path7.basename(file)));
3759
4221
  const withBase = tsconfigs.filter(({ text }) => text !== null && /"baseUrl"\s*:/.test(text));
3760
4222
  for (const { file, text } of input.compiler) {
3761
4223
  if (text === null) {
3762
4224
  continue;
3763
4225
  }
3764
4226
  const plain2 = withoutComments(text);
3765
- const name = path6.basename(file);
4227
+ const name = path7.basename(file);
3766
4228
  const swc = name === ".swcrc";
3767
4229
  if (!/^[jt]sconfig/.test(name) && !swc) {
3768
4230
  const babel = /"babel"\s*:\s*/.exec(plain2);
3769
4231
  const own2 = name !== "package.json" ? plain2 : babel === null ? "" : (() => {
3770
- const found2 = partsOf(plain2, babel.index + babel[0].length);
4232
+ const found2 = partsOf2(plain2, babel.index + babel[0].length);
3771
4233
  return found2 === null ? plain2 : found2.parts.join(",");
3772
4234
  })();
3773
4235
  const maps = /module-resolver|\balias\b/.exec(own2);
@@ -3781,7 +4243,7 @@ function unexplainedMaps(input) {
3781
4243
  continue;
3782
4244
  }
3783
4245
  const at = paths.index + paths[0].length;
3784
- const found = plain2[at] === "{" ? partsOf(plain2, at) : null;
4246
+ const found = plain2[at] === "{" ? partsOf2(plain2, at) : null;
3785
4247
  if (found === null) {
3786
4248
  reasons.push(`${shown(file)} names paths in a way that cannot be read as a list of aliases`);
3787
4249
  continue;
@@ -3791,7 +4253,7 @@ function unexplainedMaps(input) {
3791
4253
  reasons.push(`${shown(file)} names paths, and baseUrl is set in another file`);
3792
4254
  continue;
3793
4255
  }
3794
- const base = path6.resolve(path6.dirname(file), own?.[1] ?? ".");
4256
+ const base = path7.resolve(path7.dirname(file), own?.[1] ?? ".");
3795
4257
  for (const part of found.parts) {
3796
4258
  const entry = property(part);
3797
4259
  if (entry === null) {
@@ -3800,10 +4262,10 @@ function unexplainedMaps(input) {
3800
4262
  );
3801
4263
  continue;
3802
4264
  }
3803
- const list = entry.value.startsWith("[") ? partsOf(entry.value, 0) : null;
4265
+ const list = entry.value.startsWith("[") ? partsOf2(entry.value, 0) : null;
3804
4266
  const target = list?.parts.length === 1 ? stringValue(list.parts[0]) : null;
3805
4267
  const prefix = entry.key.endsWith("*") ? entry.key.slice(0, -1) : null;
3806
- if (prefix === null && target !== null && !target.includes("*") && path6.resolve(base, target).split(path6.sep).includes("node_modules")) {
4268
+ if (prefix === null && target !== null && !target.includes("*") && path7.resolve(base, target).split(path7.sep).includes("node_modules")) {
3807
4269
  continue;
3808
4270
  }
3809
4271
  const directory = target?.endsWith("*") ? target.slice(0, -1) : null;
@@ -3821,26 +4283,69 @@ function unexplainedMaps(input) {
3821
4283
  }
3822
4284
 
3823
4285
  // src/commands/mutation.ts
3824
- import { spawnSync as spawnSync2 } from "child_process";
4286
+ import { spawnSync as spawnSync4 } from "child_process";
4287
+ import fs10 from "fs";
4288
+ import path10 from "path";
4289
+ import { pathToFileURL } from "url";
4290
+
4291
+ // src/main-worktree.ts
4292
+ import { spawnSync as spawnSync3 } from "child_process";
3825
4293
  import fs8 from "fs";
3826
4294
  import path8 from "path";
3827
- import { pathToFileURL } from "url";
4295
+ function git2(dir, args) {
4296
+ const result = spawnSync3("git", args, {
4297
+ cwd: dir,
4298
+ env: gitEnvironment(process.env),
4299
+ encoding: "utf8"
4300
+ });
4301
+ return result.error || result.status !== 0 ? null : result.stdout.trim().split("\n");
4302
+ }
4303
+ function linkedWorktreeOf(dir) {
4304
+ const found = git2(dir, [
4305
+ "rev-parse",
4306
+ "--path-format=absolute",
4307
+ "--show-toplevel",
4308
+ "--git-dir",
4309
+ "--git-common-dir"
4310
+ ]);
4311
+ if (found === null || found.length !== 3) {
4312
+ return null;
4313
+ }
4314
+ const [top, gitDir, common] = found;
4315
+ if (gitDir === common || path8.basename(common) !== ".git") {
4316
+ return null;
4317
+ }
4318
+ try {
4319
+ const main2 = fs8.realpathSync(path8.dirname(common));
4320
+ const real = fs8.realpathSync(top);
4321
+ return main2 === real ? null : { top: real, main: main2 };
4322
+ } catch {
4323
+ return null;
4324
+ }
4325
+ }
4326
+ function inMainWorktree(linked, file) {
4327
+ const relative = path8.relative(linked.top, file);
4328
+ if (relative === "" || relative.startsWith("..") || path8.isAbsolute(relative)) {
4329
+ return null;
4330
+ }
4331
+ return path8.join(linked.main, relative);
4332
+ }
3828
4333
 
3829
4334
  // src/commands/mutation-result.ts
3830
- import fs7 from "fs";
3831
- import path7 from "path";
4335
+ import fs9 from "fs";
4336
+ import path9 from "path";
3832
4337
  function reportPathOf(manifest, override) {
3833
- return path7.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
4338
+ return path9.resolve(manifest.dir, override ?? manifest.settings.strykerReportFile);
3834
4339
  }
3835
- function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0) {
4340
+ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => void 0, retried = null, shard = null) {
3836
4341
  const manifest = readManifest(dir);
3837
4342
  const gates = only.length === 0 ? manifest.gates : manifest.gates.filter((gate) => only.includes(gate.path));
3838
- const caveat = only.length === 0 ? "" : gateRunCaveat(gates.length, manifest.gates.length);
4343
+ const caveat = only.length === 0 ? "" : shard === null ? gateRunCaveat(gates.length, manifest.gates.length) : shardRunCaveat(shard, gates.length, manifest.gates.length);
3839
4344
  const reportPath = reportPathOf(manifest, reportOverride);
3840
- const shown = path7.relative(manifest.dir, reportPath) || reportPath;
4345
+ const shown = path9.relative(manifest.dir, reportPath) || reportPath;
3841
4346
  let report;
3842
4347
  try {
3843
- report = JSON.parse(fs7.readFileSync(reportPath, "utf8"));
4348
+ report = JSON.parse(fs9.readFileSync(reportPath, "utf8"));
3844
4349
  } catch (error) {
3845
4350
  io2.err(
3846
4351
  `test-gates mutation: cannot read the Stryker JSON report ${shown} (${error.message}). Did Stryker finish, with the "json" reporter writing to this path?`
@@ -3857,7 +4362,7 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3857
4362
  maxTimeouts: manifest.settings.maxTimeouts,
3858
4363
  readSource: (gatePath) => {
3859
4364
  try {
3860
- return fs7.readFileSync(path7.join(manifest.dir, gatePath), "utf8");
4365
+ return fs9.readFileSync(path9.join(manifest.dir, gatePath), "utf8");
3861
4366
  } catch {
3862
4367
  return void 0;
3863
4368
  }
@@ -3905,8 +4410,34 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3905
4410
  io2.err(` - ${formatSurvivor(mutant)}`);
3906
4411
  }
3907
4412
  io2.err("");
3908
- io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
3909
- io2.err("can time out instead. Run again with less load (-- --concurrency 1). Only for a");
4413
+ if (retried === null || retried === "failed") {
4414
+ io2.err("A timeout counts as detected, but on a loaded machine a mutant that would survive");
4415
+ if (retried === null) {
4416
+ io2.err(
4417
+ "can time out instead. `test-gates mutation` runs such timeouts again one at a time"
4418
+ );
4419
+ io2.err("(--concurrency 1) before it fails; these were not run again. Only for a");
4420
+ } else {
4421
+ io2.err(
4422
+ "can time out instead. They were run again one at a time (--concurrency 1), but that"
4423
+ );
4424
+ io2.err("run gave no result (see above), so these are those of the first run. Only for a");
4425
+ }
4426
+ } else if (retried.missing === 0) {
4427
+ io2.err("They timed out again when they were run one at a time (--concurrency 1), so they");
4428
+ io2.err("most likely never finish. Look at each mutant first. Only for a");
4429
+ } else {
4430
+ const again = retried.statuses["Timeout"] ?? 0;
4431
+ if (again > 0) {
4432
+ io2.err(
4433
+ `${again} of them timed out again when they were run one at a time (--concurrency 1).`
4434
+ );
4435
+ }
4436
+ io2.err(
4437
+ `${retried.missing} of them were not in the report of the run again (--concurrency 1),`
4438
+ );
4439
+ io2.err("so they keep the timeout of the first run. Look at each mutant first. Only for a");
4440
+ }
3910
4441
  io2.err("mutant that can never finish (a loop that no longer ends), add it to");
3911
4442
  io2.err('"expectedTimeouts" of the gate in test-gates.json and fill in "reason"');
3912
4443
  io2.err("(settings.mutation.maxTimeouts raises the limit for the whole project):");
@@ -3925,16 +4456,16 @@ function runMutationResult(dir, reportOverride, io2, only = [], seen = () => voi
3925
4456
  // src/commands/mutation.ts
3926
4457
  var CONFIG_MARK = "\0test-gates-stryker-config\0";
3927
4458
  function loadStrykerConfig(manifest, config) {
3928
- const file = path8.resolve(manifest.dir, config);
4459
+ const file = path10.resolve(manifest.dir, config);
3929
4460
  if (file.endsWith(".json")) {
3930
4461
  try {
3931
- return JSON.parse(fs8.readFileSync(file, "utf8"));
4462
+ return JSON.parse(fs10.readFileSync(file, "utf8"));
3932
4463
  } catch {
3933
4464
  return void 0;
3934
4465
  }
3935
4466
  }
3936
- 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,}) + ${JSON.stringify(CONFIG_MARK)});`;
3937
- const result = spawnSync2(process.execPath, ["--input-type=module", "--eval", script], {
4467
+ 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)});`;
4468
+ const result = spawnSync4(process.execPath, ["--input-type=module", "--eval", script], {
3938
4469
  cwd: manifest.dir,
3939
4470
  encoding: "utf8",
3940
4471
  timeout: 6e4
@@ -3947,7 +4478,7 @@ function loadStrykerConfig(manifest, config) {
3947
4478
  }
3948
4479
  function modifiedAt(file) {
3949
4480
  try {
3950
- const stat = fs8.statSync(file, { bigint: true });
4481
+ const stat = fs10.statSync(file, { bigint: true });
3951
4482
  return `${stat.mtimeNs} ${stat.ino}`;
3952
4483
  } catch {
3953
4484
  return null;
@@ -3956,6 +4487,7 @@ function modifiedAt(file) {
3956
4487
  function guardRecordPath(incrementalFile) {
3957
4488
  return `${incrementalFile}.test-gates.json`;
3958
4489
  }
4490
+ var forcedLine = (reason) => `test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${reason}`;
3959
4491
  function coveringSpecs(report, gates) {
3960
4492
  const fileOf = /* @__PURE__ */ new Map();
3961
4493
  for (const [file, entry] of Object.entries(report.testFiles ?? {})) {
@@ -3982,10 +4514,20 @@ function coveringSpecs(report, gates) {
3982
4514
  }
3983
4515
  return covering;
3984
4516
  }
3985
- function decideReuse(manifest, config, strykerArgs, part, affected) {
4517
+ function readSeed(incrementalFile, recordFile) {
4518
+ try {
4519
+ const incremental = fs10.readFileSync(incrementalFile);
4520
+ const record = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
4521
+ const named = record?.incremental;
4522
+ return named === hashOf(incremental) ? { incremental, record } : null;
4523
+ } catch {
4524
+ return null;
4525
+ }
4526
+ }
4527
+ function decideReuse(manifest, config, strykerArgs, part, affected, found = () => void 0) {
3986
4528
  const loaded = loadStrykerConfig(manifest, config);
3987
4529
  const options = incrementalOptions(loaded, strykerArgs);
3988
- const base = fs8.realpathSync(manifest.dir);
4530
+ const base = fs10.realpathSync(manifest.dir);
3989
4531
  const checkersArg = strykerArgs.some(
3990
4532
  (arg) => arg === "--checkers" || arg.startsWith("--checkers=")
3991
4533
  );
@@ -4009,35 +4551,42 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4009
4551
  last: void 0,
4010
4552
  ci: affected ? affectedRefused(process.env) : null,
4011
4553
  unchanged: null,
4012
- configs
4554
+ configs,
4555
+ temp: {
4556
+ dir: loaded === void 0 ? null : path10.resolve(base, options.tempDirName),
4557
+ keeps: loaded?.cleanTempDir === false
4558
+ },
4559
+ mainWorktree: null,
4560
+ seed: null
4013
4561
  };
4562
+ found(decision.temp);
4014
4563
  if (loaded !== void 0 && !options.incremental) {
4015
4564
  return decision;
4016
4565
  }
4017
4566
  decision.incremental = true;
4018
- const incrementalFile = path8.resolve(base, options.incrementalFile);
4567
+ const incrementalFile = path10.resolve(base, options.incrementalFile);
4019
4568
  const recordFile = guardRecordPath(incrementalFile);
4020
4569
  decision.incrementalFile = incrementalFile;
4021
4570
  decision.recordFile = recordFile;
4022
4571
  let snapshot = null;
4023
4572
  let root = base;
4024
4573
  let failure = "the Stryker config could not be loaded to see where the incremental file is";
4025
- const reports = reportFiles(loaded).map((file) => path8.resolve(base, file));
4574
+ const reports = reportFiles(loaded).map((file) => path10.resolve(base, file));
4026
4575
  const ignores = strykerIgnores(loaded);
4027
4576
  const read2 = (file) => {
4028
- const relative = path8.relative(base, file);
4029
- const inside = !relative.startsWith("..") && !path8.isAbsolute(relative);
4030
- return !reports.includes(file) && !(inside && ignores(relative.split(path8.sep).join("/")));
4577
+ const relative = path10.relative(base, file);
4578
+ const inside = !relative.startsWith("..") && !path10.isAbsolute(relative);
4579
+ return !reports.includes(file) && !(inside && ignores(relative.split(path10.sep).join("/")));
4031
4580
  };
4032
4581
  if (loaded !== void 0) {
4033
4582
  const scope = (listed2) => {
4034
4583
  try {
4035
- const found = loadedScope(manifest, configs(), listed2);
4036
- if (!("files" in found)) {
4037
- decision.scope = found;
4584
+ const found2 = loadedScope(manifest, configs(), listed2);
4585
+ if (!("files" in found2)) {
4586
+ decision.scope = found2;
4038
4587
  return null;
4039
4588
  }
4040
- const files = new Set([...found.files].filter(read2));
4589
+ const files = new Set([...found2.files].filter(read2));
4041
4590
  decision.scope = { files: files.size };
4042
4591
  return files;
4043
4592
  } catch (error) {
@@ -4053,8 +4602,8 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4053
4602
  written: [
4054
4603
  incrementalFile,
4055
4604
  recordFile,
4056
- path8.resolve(base, options.tempDirName),
4057
- path8.resolve(base, manifest.settings.strykerReportFile),
4605
+ path10.resolve(base, options.tempDirName),
4606
+ path10.resolve(base, manifest.settings.strykerReportFile),
4058
4607
  ...reports
4059
4608
  ],
4060
4609
  args: comparableArgs(strykerArgs)
@@ -4067,17 +4616,32 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4067
4616
  decision.snapshot = snapshot;
4068
4617
  decision.failure = snapshot === null ? failure : null;
4069
4618
  decision.root = root;
4070
- const incremental = hashFile(incrementalFile);
4619
+ const tempDir = path10.basename(options.tempDirName);
4620
+ if (!part && !options.force && snapshot?.mode === "git" && !path10.relative(root, base).split(path10.sep).includes(tempDir) && !fs10.existsSync(incrementalFile) && !fs10.existsSync(recordFile)) {
4621
+ const linked = linkedWorktreeOf(base);
4622
+ const mainIncremental = linked === null ? null : inMainWorktree(linked, incrementalFile);
4623
+ const mainRecord = linked === null ? null : inMainWorktree(linked, recordFile);
4624
+ if (linked !== null && mainIncremental !== null && mainRecord !== null) {
4625
+ decision.mainWorktree = linked.main;
4626
+ const seed = readSeed(mainIncremental, mainRecord);
4627
+ if (seed !== null) {
4628
+ decision.seed = seed;
4629
+ }
4630
+ }
4631
+ }
4632
+ const incremental = decision.seed === null ? hashFile(incrementalFile) : hashOf(decision.seed.incremental);
4071
4633
  decision.incrementalHash = incremental;
4072
4634
  if (part) {
4073
4635
  decision.force = incremental !== null && !options.force ? "with --gate nothing is taken over from an earlier run" : null;
4074
4636
  return decision;
4075
4637
  }
4076
- let stored;
4077
- try {
4078
- stored = JSON.parse(fs8.readFileSync(recordFile, "utf8"));
4079
- } catch {
4080
- stored = void 0;
4638
+ let stored = decision.seed?.record;
4639
+ if (decision.seed === null) {
4640
+ try {
4641
+ stored = JSON.parse(fs10.readFileSync(recordFile, "utf8"));
4642
+ } catch {
4643
+ stored = void 0;
4644
+ }
4081
4645
  }
4082
4646
  const ci = decision.ci;
4083
4647
  let force = null;
@@ -4091,6 +4655,9 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4091
4655
  if (force === null) {
4092
4656
  const read3 = readRecord({ record: stored, incremental, snapshot });
4093
4657
  last = "record" in read3 ? read3.record.affected : void 0;
4658
+ if (decision.seed !== null) {
4659
+ force = blindInMainRecord(manifest, { root, snapshot, configs, last });
4660
+ }
4094
4661
  }
4095
4662
  } else if (ci !== null) {
4096
4663
  force = ci;
@@ -4117,8 +4684,10 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4117
4684
  config: inputs.config,
4118
4685
  gates: manifest.gates.map((gate) => ({
4119
4686
  path: gate.path,
4120
- files: inputs.gates[gate.path]?.files ?? []
4121
- }))
4687
+ files: inputs.gates[gate.path]?.files ?? [],
4688
+ blind: inputs.gates[gate.path]?.blind
4689
+ })),
4690
+ seeded: decision.seed !== null
4122
4691
  });
4123
4692
  } catch (error) {
4124
4693
  if (!(error instanceof AffectedError)) {
@@ -4127,7 +4696,7 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4127
4696
  plan = { full: `what a gate depends on cannot be told: ${error.message}` };
4128
4697
  }
4129
4698
  if ("full" in plan) {
4130
- if (refuseReuse({ record: stored, incremental, snapshot }) === null) {
4699
+ if (decision.seed === null && refuseReuse({ record: stored, incremental, snapshot }) === null) {
4131
4700
  decision.unchanged = plan.full;
4132
4701
  plan = null;
4133
4702
  } else {
@@ -4144,9 +4713,45 @@ function decideReuse(manifest, config, strykerArgs, part, affected) {
4144
4713
  decision.last = last;
4145
4714
  return decision;
4146
4715
  }
4147
- function guardIncremental(manifest, config, strykerArgs, part, affected) {
4148
- const nothing = { force: null, mutate: null, said: [], record: () => void 0 };
4149
- const decision = decideReuse(manifest, config, strykerArgs, part, affected);
4716
+ function blindInMainRecord(manifest, input) {
4717
+ const { last } = input;
4718
+ const unknown = last === void 0 ? "each gate" : manifest.gates.find((gate) => !Object.hasOwn(last.gates, gate.path))?.path;
4719
+ if (last === void 0 || unknown !== void 0) {
4720
+ 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`;
4721
+ }
4722
+ try {
4723
+ const inputs = affectedInputs(manifest, {
4724
+ root: input.root,
4725
+ files: input.snapshot.files,
4726
+ configs: input.configs(),
4727
+ tests: last.tests,
4728
+ covering: Object.fromEntries(
4729
+ Object.entries(last.gates).map(([gate, entry]) => [gate, entry.specs])
4730
+ )
4731
+ });
4732
+ for (const gate of manifest.gates) {
4733
+ const blind = inputs.gates[gate.path]?.blind ?? last.gates[gate.path]?.blind;
4734
+ if (blind !== void 0) {
4735
+ return `${gate.path} may read files git does not list, which differ between worktrees (${blind})`;
4736
+ }
4737
+ }
4738
+ return null;
4739
+ } catch (error) {
4740
+ if (!(error instanceof AffectedError)) {
4741
+ throw error;
4742
+ }
4743
+ return `what a gate depends on cannot be told, so it may read files git does not list, which differ between worktrees: ${error.message}`;
4744
+ }
4745
+ }
4746
+ function guardIncremental(manifest, config, strykerArgs, part, affected, found) {
4747
+ const decision = decideReuse(manifest, config, strykerArgs, part, affected, found);
4748
+ const nothing = {
4749
+ force: null,
4750
+ mutate: null,
4751
+ said: [],
4752
+ temp: decision.temp,
4753
+ record: () => void 0
4754
+ };
4150
4755
  if (!decision.incremental) {
4151
4756
  return affected ? {
4152
4757
  ...nothing,
@@ -4161,10 +4766,14 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4161
4766
  const incremental = decision.incrementalHash;
4162
4767
  const options = { force: decision.forced };
4163
4768
  if (part) {
4164
- fs8.rmSync(recordFile, { force: true });
4769
+ fs10.rmSync(recordFile, { force: true });
4165
4770
  return { ...nothing, force };
4166
4771
  }
4167
- fs8.rmSync(recordFile, { force: true });
4772
+ if (decision.seed !== null) {
4773
+ fs10.mkdirSync(path10.dirname(incrementalFile), { recursive: true });
4774
+ fs10.writeFileSync(incrementalFile, decision.seed.incremental);
4775
+ }
4776
+ fs10.rmSync(recordFile, { force: true });
4168
4777
  const before = modifiedAt(incrementalFile);
4169
4778
  const rerun = plan !== null && "rerun" in plan ? plan.rerun.map((entry) => entry.gate) : null;
4170
4779
  const everyGate = force ?? (rerun !== null && rerun.length > 0 && rerun.length === manifest.gates.length ? "every gate is affected" : null);
@@ -4179,7 +4788,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4179
4788
  rerun: timedOut,
4180
4789
  expected
4181
4790
  } = timeoutRanges(
4182
- JSON.parse(fs8.readFileSync(incrementalFile, "utf8")),
4791
+ JSON.parse(fs10.readFileSync(incrementalFile, "utf8")),
4183
4792
  kept
4184
4793
  ));
4185
4794
  } catch {
@@ -4210,11 +4819,20 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4210
4819
  said.push(`test-gates mutation: ${describeExpected(expected)}`);
4211
4820
  }
4212
4821
  }
4822
+ if (decision.seed !== null) {
4823
+ if (said.length === 0 && everyGate !== null) {
4824
+ said.push(forcedLine(everyGate));
4825
+ }
4826
+ said.unshift(
4827
+ `test-gates mutation: this worktree has no record of its own, so it starts from the one of the main worktree (${decision.mainWorktree})`
4828
+ );
4829
+ }
4213
4830
  return {
4214
4831
  // Every gate is run again: the same as a run of every mutant.
4215
4832
  force: everyGate,
4216
4833
  mutate: subset.length + ranges.length > 0 ? [...subset, ...ranges] : null,
4217
4834
  said,
4835
+ temp: decision.temp,
4218
4836
  record: (judged) => {
4219
4837
  const written = hashFile(incrementalFile);
4220
4838
  if (snapshot === null || written === null || modifiedAt(incrementalFile) === before) {
@@ -4225,7 +4843,7 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4225
4843
  record.affected = affectedRecord(judged, ran, last);
4226
4844
  }
4227
4845
  try {
4228
- fs8.writeFileSync(recordFile, `${JSON.stringify(record)}
4846
+ fs10.writeFileSync(recordFile, `${JSON.stringify(record)}
4229
4847
  `);
4230
4848
  } catch {
4231
4849
  }
@@ -4256,7 +4874,11 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4256
4874
  for (const gate of Object.keys(covering)) {
4257
4875
  const entry = inputs.gates[gate];
4258
4876
  if (entry !== void 0) {
4259
- recorded[gate] = { specs: entry.specs, files: entry.files };
4877
+ recorded[gate] = {
4878
+ specs: entry.specs,
4879
+ files: entry.files,
4880
+ ...entry.blind === void 0 ? {} : { blind: entry.blind }
4881
+ };
4260
4882
  }
4261
4883
  }
4262
4884
  return { tests: Object.keys(judged.report.testFiles ?? {}).sort(), gates: recorded };
@@ -4268,11 +4890,11 @@ function guardIncremental(manifest, config, strykerArgs, part, affected) {
4268
4890
  }
4269
4891
  }
4270
4892
  }
4271
- function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
4893
+ function runMutation(dir, strykerArgs, io2, only = [], affected = false, shard = null) {
4272
4894
  const part = only.length > 0;
4273
- if (affected && strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
4895
+ if ((affected || shard !== null) && strykerArgs.some((arg) => /^(?:--mutate|-m)(?:=|$)/.test(arg))) {
4274
4896
  throw new UsageError(
4275
- "--affected and --mutate cannot be combined: --affected sets --mutate itself"
4897
+ `${affected ? "--affected" : "--shard"} and --mutate cannot be combined: ${affected ? "--affected" : "--shard"} sets --mutate itself`
4276
4898
  );
4277
4899
  }
4278
4900
  if (part) {
@@ -4289,11 +4911,12 @@ function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
4289
4911
  gates: manifest.gates.map((gate) => gate.path),
4290
4912
  mode: "all",
4291
4913
  only,
4292
- shard: null
4914
+ shard
4293
4915
  });
4294
4916
  if ("error" in chosen) {
4295
4917
  throw new UsageError(chosen.error);
4296
4918
  }
4919
+ const shardGates = shard === null ? null : chosen.selected;
4297
4920
  const config = findStrykerConfig(manifest);
4298
4921
  if (config === null) {
4299
4922
  throw new ManifestError(
@@ -4307,53 +4930,122 @@ function runMutation(dir, strykerArgs, io2, only = [], affected = false) {
4307
4930
  );
4308
4931
  }
4309
4932
  const reportFile = reportPathOf(manifest, null);
4310
- fs8.rmSync(reportFile, { force: true });
4311
- const guard = guardIncremental(manifest, config, strykerArgs, part, affected);
4933
+ fs10.rmSync(reportFile, { force: true });
4934
+ let removed = [];
4935
+ const guard = guardIncremental(manifest, config, strykerArgs, part, affected, (temp) => {
4936
+ removed = removeStaleSandboxes(manifest.dir, temp);
4937
+ });
4312
4938
  if (guard.said.length > 0) {
4313
4939
  guard.said.forEach((line) => io2.out(line));
4314
4940
  } else if (guard.force !== null) {
4315
- io2.out(
4316
- `test-gates mutation: running every mutant again (--force), the results of the last run are not reused: ${guard.force}`
4317
- );
4941
+ io2.out(forcedLine(guard.force));
4318
4942
  }
4943
+ removed.forEach((line) => io2.out(line));
4319
4944
  const guardArgs = guard.force === null && guard.mutate === null ? [] : ["--force"];
4320
- const partArgs = part ? ["--mutate", chosen.selected.join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
4321
- const result = spawnSync2(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
4322
- cwd: manifest.dir,
4323
- stdio: "inherit",
4324
- shell: process.platform === "win32"
4325
- });
4326
- if (result.error) {
4327
- io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
4328
- return 1;
4329
- }
4330
- if (result.status !== 0) {
4331
- io2.err(
4332
- `test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
4945
+ const partArgs = part || shardGates !== null ? ["--mutate", (shardGates ?? chosen.selected).join(",")] : guard.mutate !== null ? ["--mutate", guard.mutate.join(",")] : [];
4946
+ const judged = part ? chosen.selected : shardGates ?? [];
4947
+ const unmark = markRun(manifest.dir, guard.temp);
4948
+ let retried;
4949
+ try {
4950
+ const result = spawnSync4(stryker, ["run", config, ...strykerArgs, ...partArgs, ...guardArgs], {
4951
+ cwd: manifest.dir,
4952
+ stdio: "inherit",
4953
+ shell: process.platform === "win32"
4954
+ });
4955
+ if (result.error) {
4956
+ io2.err(`test-gates mutation: cannot start Stryker (${result.error.message})`);
4957
+ return 1;
4958
+ }
4959
+ if (result.status !== 0) {
4960
+ io2.err(
4961
+ `test-gates mutation: Stryker failed (${result.signal ? `signal ${result.signal}` : `exit ${result.status}`}). The result was not judged`
4962
+ );
4963
+ return 1;
4964
+ }
4965
+ retried = retryTimeouts(
4966
+ stryker,
4967
+ [config, ...strykerArgs],
4968
+ manifest,
4969
+ reportFile,
4970
+ judged.length > 0 ? manifest.gates.filter((gate) => judged.includes(gate.path)) : manifest.gates,
4971
+ io2
4333
4972
  );
4334
- return 1;
4973
+ } finally {
4974
+ unmark();
4335
4975
  }
4336
- let judged = null;
4337
- const status = runMutationResult(dir, null, io2, part ? chosen.selected : [], (seen) => {
4338
- judged = seen;
4339
- });
4340
- guard.record(judged);
4976
+ let lastSeen = null;
4977
+ const status = runMutationResult(
4978
+ dir,
4979
+ null,
4980
+ io2,
4981
+ judged,
4982
+ (seen) => {
4983
+ lastSeen = seen;
4984
+ },
4985
+ retried,
4986
+ shard
4987
+ );
4988
+ guard.record(lastSeen);
4341
4989
  if (part) {
4342
- fs8.rmSync(reportFile, { force: true });
4990
+ fs10.rmSync(reportFile, { force: true });
4343
4991
  }
4344
4992
  return status;
4345
4993
  }
4994
+ function retryTimeouts(stryker, args, manifest, reportFile, gates, io2) {
4995
+ let first;
4996
+ try {
4997
+ first = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
4998
+ } catch {
4999
+ return null;
5000
+ }
5001
+ if (typeof first !== "object" || first === null) {
5002
+ return null;
5003
+ }
5004
+ const { ranges, rerun } = timeoutRanges(first, gates);
5005
+ if (ranges.length === 0 || rerun <= manifest.settings.maxTimeouts) {
5006
+ return null;
5007
+ }
5008
+ io2.out(
5009
+ `test-gates mutation: ${rerun} mutant(s) timed out outside expectedTimeouts, running them again one at a time (--concurrency 1)`
5010
+ );
5011
+ fs10.rmSync(reportFile, { force: true });
5012
+ const result = spawnSync4(
5013
+ stryker,
5014
+ ["run", ...args, "--mutate", ranges.join(","), "--concurrency", "1", "--force"],
5015
+ { cwd: manifest.dir, stdio: "inherit", shell: process.platform === "win32" }
5016
+ );
5017
+ let retry = null;
5018
+ if (!result.error && result.status === 0) {
5019
+ try {
5020
+ retry = JSON.parse(fs10.readFileSync(reportFile, "utf8"));
5021
+ } catch {
5022
+ retry = null;
5023
+ }
5024
+ }
5025
+ if (retry === null || typeof retry !== "object") {
5026
+ const why = result.error ? result.error.message : result.signal ? `signal ${result.signal}` : result.status === 0 ? "no report" : `exit ${result.status}`;
5027
+ io2.err(
5028
+ `test-gates mutation: running the timed-out mutants again failed (${why}). The first run is judged`
5029
+ );
5030
+ fs10.writeFileSync(reportFile, JSON.stringify(first));
5031
+ return "failed";
5032
+ }
5033
+ const merged = mergeRetry(first, retry, gates);
5034
+ fs10.writeFileSync(reportFile, JSON.stringify(merged.report));
5035
+ io2.out(formatRetry(merged.outcome));
5036
+ return merged.outcome;
5037
+ }
4346
5038
 
4347
5039
  // src/commands/doctor.ts
4348
5040
  function read(file) {
4349
5041
  try {
4350
- return fs9.readFileSync(file, "utf8");
5042
+ return fs11.readFileSync(file, "utf8");
4351
5043
  } catch {
4352
5044
  return null;
4353
5045
  }
4354
5046
  }
4355
- function git2(dir, args) {
4356
- const result = spawnSync3("git", args, {
5047
+ function git3(dir, args) {
5048
+ const result = spawnSync5("git", args, {
4357
5049
  cwd: dir,
4358
5050
  env: gitEnvironment(process.env),
4359
5051
  encoding: "utf8"
@@ -4361,20 +5053,20 @@ function git2(dir, args) {
4361
5053
  return result.status === 0 ? result.stdout.trim() : null;
4362
5054
  }
4363
5055
  function hooksOf(dir) {
4364
- const root = git2(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
4365
- const hooksDir = git2(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
5056
+ const root = git3(dir, ["rev-parse", "--show-toplevel"]) ?? dir;
5057
+ const hooksDir = git3(dir, ["rev-parse", "--path-format=absolute", "--git-path", "hooks"]);
4366
5058
  const candidates = [
4367
- ...["pre-push", "pre-commit"].map((name) => path9.join(root, ".husky", name)),
4368
- ...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path9.join(hooksDir, name)),
4369
- ...["lefthook.yml", ".lefthook.yml"].map((name) => path9.join(root, name))
5059
+ ...["pre-push", "pre-commit"].map((name) => path11.join(root, ".husky", name)),
5060
+ ...hooksDir === null ? [] : ["pre-push", "pre-commit"].map((name) => path11.join(hooksDir, name)),
5061
+ ...["lefthook.yml", ".lefthook.yml"].map((name) => path11.join(root, name))
4370
5062
  ];
4371
5063
  let script = "";
4372
5064
  try {
4373
- const manifest = JSON.parse(read(path9.join(dir, "package.json")) ?? "{}");
5065
+ const manifest = JSON.parse(read(path11.join(dir, "package.json")) ?? "{}");
4374
5066
  script = manifest.scripts?.["test:gates:mutation"] ?? "";
4375
5067
  } catch {
4376
5068
  }
4377
- const subproject = path9.relative(fs9.realpathSync(root), dir).split(path9.sep).join("/");
5069
+ const subproject = path11.relative(fs11.realpathSync(root), dir).split(path11.sep).join("/");
4378
5070
  const seen = /* @__PURE__ */ new Set();
4379
5071
  const uses = [];
4380
5072
  for (const file of candidates) {
@@ -4382,12 +5074,12 @@ function hooksOf(dir) {
4382
5074
  if (text === null) {
4383
5075
  continue;
4384
5076
  }
4385
- const real = fs9.realpathSync(file);
5077
+ const real = fs11.realpathSync(file);
4386
5078
  if (seen.has(real)) {
4387
5079
  continue;
4388
5080
  }
4389
5081
  seen.add(real);
4390
- const shown = path9.relative(root, file).split(path9.sep).join("/");
5082
+ const shown = path11.relative(root, file).split(path11.sep).join("/");
4391
5083
  uses.push(...hookUses({ file: shown, text, subproject, script }));
4392
5084
  }
4393
5085
  return uses;
@@ -4404,8 +5096,8 @@ function runDoctor(dir, io2) {
4404
5096
  const affected = decideReuse(manifest, config, [], false, true);
4405
5097
  io2.out(`test-gates doctor: ${manifest.dir} (${config})`);
4406
5098
  if (plain2.snapshot !== null) {
4407
- const subproject = path9.relative(plain2.root, fs9.realpathSync(manifest.dir));
4408
- const prefix = subproject === "" ? "" : `${subproject.split(path9.sep).join("/")}/`;
5099
+ const subproject = path11.relative(plain2.root, fs11.realpathSync(manifest.dir));
5100
+ const prefix = subproject === "" ? "" : `${subproject.split(path11.sep).join("/")}/`;
4409
5101
  const keys = Object.keys(plain2.snapshot.files);
4410
5102
  const outside = keys.filter((key) => !key.startsWith(prefix)).length;
4411
5103
  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";
@@ -4416,6 +5108,15 @@ function runDoctor(dir, io2) {
4416
5108
  io2.out(
4417
5109
  reuse === null ? " next run: reuses the results of the last run" : ` next run: every mutant, ${reuse}`
4418
5110
  );
5111
+ if (plain2.seed !== null) {
5112
+ io2.out(
5113
+ ` this worktree has no record of its own: the next run starts from the one of the main worktree (${plain2.mainWorktree})`
5114
+ );
5115
+ } else if (plain2.mainWorktree !== null) {
5116
+ io2.out(
5117
+ ` this worktree has no record of its own, and the main worktree (${plain2.mainWorktree}) has none this worktree can start from`
5118
+ );
5119
+ }
4419
5120
  if (affected.ci !== null) {
4420
5121
  io2.out(` next run with --affected: every mutant, ${affected.ci}`);
4421
5122
  } else if (affected.plan !== null && "rerun" in affected.plan) {
@@ -4432,15 +5133,20 @@ function runDoctor(dir, io2) {
4432
5133
  }
4433
5134
  }
4434
5135
  let blind = null;
5136
+ let blindGates = [];
4435
5137
  if (plain2.snapshot !== null) {
4436
5138
  try {
4437
- affectedInputs(manifest, {
5139
+ const inputs = affectedInputs(manifest, {
4438
5140
  root: plain2.root,
4439
5141
  files: plain2.snapshot.files,
4440
5142
  configs: plain2.configs(),
4441
5143
  tests: [],
4442
5144
  covering: {}
4443
5145
  });
5146
+ blindGates = manifest.gates.flatMap((gate) => {
5147
+ const reason = inputs.gates[gate.path]?.blind;
5148
+ return reason === void 0 ? [] : [{ gate: gate.path, reason }];
5149
+ });
4444
5150
  } catch (error) {
4445
5151
  if (!(error instanceof AffectedError)) {
4446
5152
  throw error;
@@ -4448,6 +5154,8 @@ function runDoctor(dir, io2) {
4448
5154
  blind = error.message;
4449
5155
  }
4450
5156
  }
5157
+ const left = sandboxPlan(manifest.dir, plain2.temp);
5158
+ const count = "kept" in left.plan ? left.plan.kept.length : left.plan.remove.length;
4451
5159
  const { lines, status } = formatFindings(
4452
5160
  diagnose({
4453
5161
  incremental: plain2.incremental,
@@ -4455,8 +5163,16 @@ function runDoctor(dir, io2) {
4455
5163
  failure: plain2.failure,
4456
5164
  widened: plain2.widened,
4457
5165
  scope: plain2.scope,
4458
- hooks: hooksOf(fs9.realpathSync(manifest.dir)),
4459
- blind
5166
+ hooks: hooksOf(fs11.realpathSync(manifest.dir)),
5167
+ blind,
5168
+ blindGates,
5169
+ gates: manifest.gates.length,
5170
+ sandboxes: count === 0 ? null : {
5171
+ tempDir: left.shown,
5172
+ count,
5173
+ kept: "kept" in left.plan ? left.plan.why : null,
5174
+ running: "kept" in left.plan ? left.plan.running : null
5175
+ }
4460
5176
  })
4461
5177
  );
4462
5178
  lines.forEach((line) => io2.out(line));
@@ -4464,8 +5180,8 @@ function runDoctor(dir, io2) {
4464
5180
  }
4465
5181
 
4466
5182
  // src/commands/lcov.ts
4467
- import fs10 from "fs";
4468
- import path10 from "path";
5183
+ import fs12 from "fs";
5184
+ import path12 from "path";
4469
5185
 
4470
5186
  // src/core/lcov.ts
4471
5187
  function normalizePath2(file, rootDir) {
@@ -4492,9 +5208,9 @@ function parseLcov(text, rootDir) {
4492
5208
  const line = rawLine.trim();
4493
5209
  if (line.startsWith("SF:")) {
4494
5210
  closeRecord();
4495
- const path13 = normalizePath2(line.slice(3), rootDir);
4496
- const file = files.get(path13) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
4497
- files.set(path13, file);
5211
+ const path15 = normalizePath2(line.slice(3), rootDir);
5212
+ const file = files.get(path15) ?? { lines: /* @__PURE__ */ new Map(), found: 0, hit: 0 };
5213
+ files.set(path15, file);
4498
5214
  record = { file, hasDa: false, found: 0, hit: 0 };
4499
5215
  } else if (line === "end_of_record") {
4500
5216
  closeRecord();
@@ -4578,10 +5294,10 @@ function runLcov(dir, fileOption, io2) {
4578
5294
  "no lcov file given. Pass --file <lcov.info> or set settings.lcov.file"
4579
5295
  );
4580
5296
  }
4581
- const lcovPath = path10.resolve(manifest.dir, file);
5297
+ const lcovPath = path12.resolve(manifest.dir, file);
4582
5298
  let text;
4583
5299
  try {
4584
- text = fs10.readFileSync(lcovPath, "utf8");
5300
+ text = fs12.readFileSync(lcovPath, "utf8");
4585
5301
  } catch (error) {
4586
5302
  io2.err(
4587
5303
  `test-gates lcov: cannot read ${file} (${error.message}). Run the tests with coverage first`
@@ -4624,10 +5340,10 @@ function runLcov(dir, fileOption, io2) {
4624
5340
  }
4625
5341
 
4626
5342
  // src/commands/selfcheck.ts
4627
- import { spawn, spawnSync as spawnSync4 } from "child_process";
4628
- import fs12 from "fs";
5343
+ import { spawn, spawnSync as spawnSync6 } from "child_process";
5344
+ import fs14 from "fs";
4629
5345
  import os2 from "os";
4630
- import path12 from "path";
5346
+ import path14 from "path";
4631
5347
 
4632
5348
  // src/core/jobs.ts
4633
5349
  var SWITCHES = {
@@ -4667,6 +5383,34 @@ function knownCommand(program, args) {
4667
5383
  function jobFlags(runner, dir) {
4668
5384
  return runner === "jest" ? [`--coverageDirectory=${dir}/coverage`, `--cacheDirectory=${dir}/cache`] : [`--coverage.reportsDirectory=${dir}/coverage`, "--no-cache"];
4669
5385
  }
5386
+ var LIMITS = {
5387
+ jest: ["--runInBand", "-i", "--maxWorkers", "-w"],
5388
+ vitest: ["--no-file-parallelism", "--fileParallelism", "--maxWorkers", "--minWorkers"]
5389
+ };
5390
+ var CONFIG_LIMITS = [
5391
+ "maxWorkers",
5392
+ "minWorkers",
5393
+ "maxThreads",
5394
+ "minThreads",
5395
+ "maxForks",
5396
+ "minForks",
5397
+ "fileParallelism",
5398
+ "singleThread",
5399
+ "singleFork"
5400
+ ];
5401
+ function workerFlags(input) {
5402
+ if (input.lines <= 1) {
5403
+ return [];
5404
+ }
5405
+ const given = input.args.some(
5406
+ (arg) => LIMITS[input.runner].includes(arg.split("=")[0])
5407
+ );
5408
+ const configured = CONFIG_LIMITS.some((word) => input.configSource?.includes(word) === true);
5409
+ if (given || configured) {
5410
+ return [];
5411
+ }
5412
+ return [`--maxWorkers=${Math.max(1, Math.floor(input.cores / input.lines))}`];
5413
+ }
4670
5414
  var REACHES = [
4671
5415
  "process.",
4672
5416
  "process?.",
@@ -4690,6 +5434,11 @@ var REACHES = [
4690
5434
  "WebSocket",
4691
5435
  "XMLHttpRequest",
4692
5436
  "navigator",
5437
+ // What crypto keeps as a setting of its module, for every spec file of a worker after it.
5438
+ "setFips",
5439
+ "setEngine",
5440
+ "setDefaultEncoding",
5441
+ "DEFAULT_ENCODING",
4693
5442
  "Atomics",
4694
5443
  "WeakRef",
4695
5444
  "FinalizationRegistry",
@@ -4698,10 +5447,35 @@ var REACHES = [
4698
5447
  ];
4699
5448
  var COMPUTED_LOAD = /(?:require|import)\((?!['"])|\brequire(?!\(|\.resolve\b|[\w$])|\bimport\.meta\.glob\b/;
4700
5449
  var COMPUTED = "loads a module whose name is computed";
5450
+ var STRING = /(^|[:,([={?+&|;])(\s*)(['"])((?:\\.|(?!\3)[^\\\n])*)\3(?=\s*(?:[,}\]);:+?&|]|$))/gm;
5451
+ function withoutStrings(source) {
5452
+ return source.replace(
5453
+ STRING,
5454
+ (all, before, space, quote, inside) => (
5455
+ // In a template (`{"home": "${home()}"}`) the quotes are text, and what is
5456
+ // between them may be code.
5457
+ inside.includes("${") ? all : `${before}${space}${quote}${" ".repeat(inside.length)}${quote}`
5458
+ )
5459
+ );
5460
+ }
5461
+ var PURE_BUILTINS = [
5462
+ "assert",
5463
+ "assert/strict",
5464
+ "crypto",
5465
+ "path",
5466
+ "path/posix",
5467
+ "path/win32",
5468
+ "punycode",
5469
+ "querystring",
5470
+ "string_decoder",
5471
+ "url",
5472
+ "util/types"
5473
+ ];
4701
5474
  var RUNNER_PACKAGES = ["vitest", "@jest/globals"];
4702
5475
  function reachesOutside(file, trusted) {
4703
5476
  const dense = file.source.replace(/\s/g, "");
4704
- const mark = REACHES.find((text) => dense.includes(text));
5477
+ const code = withoutStrings(file.source).replace(/\s/g, "");
5478
+ const mark = REACHES.find((text) => code.includes(text));
4705
5479
  if (mark !== void 0) {
4706
5480
  return `uses ${mark.replace(/[.?[(:]+$/, "")}`;
4707
5481
  }
@@ -4710,7 +5484,9 @@ function reachesOutside(file, trusted) {
4710
5484
  }
4711
5485
  for (const { specifier, builtin } of file.imports) {
4712
5486
  if (builtin) {
4713
- return `imports ${specifier}`;
5487
+ if (!PURE_BUILTINS.includes(specifier.replace(/^node:/, ""))) {
5488
+ return `imports ${specifier}`;
5489
+ }
4714
5490
  } else if (![...RUNNER_PACKAGES, ...trusted].some(
4715
5491
  (name) => specifier === name || specifier.startsWith(`${name}/`)
4716
5492
  )) {
@@ -4844,8 +5620,8 @@ function cleanHold(input) {
4844
5620
 
4845
5621
  // src/touch.ts
4846
5622
  import { createHash as createHash2 } from "crypto";
4847
- import fs11 from "fs";
4848
- import path11 from "path";
5623
+ import fs13 from "fs";
5624
+ import path13 from "path";
4849
5625
  import { fileURLToPath } from "url";
4850
5626
  var TOUCH_ENV = "TEST_GATES_TOUCH_DIR";
4851
5627
  function isObject(value) {
@@ -4879,7 +5655,7 @@ function isCounters(value) {
4879
5655
  function readNotes(directory) {
4880
5656
  const read2 = (file) => {
4881
5657
  try {
4882
- return JSON.parse(fs11.readFileSync(path11.join(directory, file), "utf8"));
5658
+ return JSON.parse(fs13.readFileSync(path13.join(directory, file), "utf8"));
4883
5659
  } catch {
4884
5660
  return null;
4885
5661
  }
@@ -4887,8 +5663,8 @@ function readNotes(directory) {
4887
5663
  const run = read2("run.json");
4888
5664
  let notes = [];
4889
5665
  try {
4890
- for (const name of fs11.readdirSync(path11.join(directory, "specs"))) {
4891
- const note = read2(path11.join("specs", name));
5666
+ for (const name of fs13.readdirSync(path13.join(directory, "specs"))) {
5667
+ const note = read2(path13.join("specs", name));
4892
5668
  if (!isObject(note) || typeof note.spec !== "string" || !isObject(note.files) || !Object.values(note.files).every(isCounters)) {
4893
5669
  throw new Error("not a note");
4894
5670
  }
@@ -4900,7 +5676,7 @@ function readNotes(directory) {
4900
5676
  return {
4901
5677
  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,
4902
5678
  notes,
4903
- totals: countersOfAll(read2(path11.join("coverage", "coverage-final.json")))
5679
+ totals: countersOfAll(read2(path13.join("coverage", "coverage-final.json")))
4904
5680
  };
4905
5681
  }
4906
5682
 
@@ -4915,7 +5691,7 @@ function resolveGateCommand(manifest) {
4915
5691
  if (manifest.settings.gateCommand !== null) {
4916
5692
  return manifest.settings.gateCommand;
4917
5693
  }
4918
- const exists = (name) => fs12.existsSync(path12.join(manifest.dir, name));
5694
+ const exists = (name) => fs14.existsSync(path14.join(manifest.dir, name));
4919
5695
  const jest = JEST_CONFIGS.find(exists);
4920
5696
  const vitest = VITEST_CONFIGS.find(exists);
4921
5697
  if (jest && vitest) {
@@ -4936,7 +5712,7 @@ function resolveGateCommand(manifest) {
4936
5712
  var TOUCH_PREFIX = ".test-gates-touch-";
4937
5713
  function realPath(file) {
4938
5714
  try {
4939
- return fs12.realpathSync(file);
5715
+ return fs14.realpathSync(file);
4940
5716
  } catch {
4941
5717
  return file;
4942
5718
  }
@@ -4944,13 +5720,16 @@ function realPath(file) {
4944
5720
  function shareOneRun(manifest, targets, runGate) {
4945
5721
  let touchDir;
4946
5722
  try {
4947
- for (const name of fs12.readdirSync(manifest.dir)) {
4948
- const left = path12.join(manifest.dir, name);
4949
- if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
4950
- fs12.rmSync(left, { recursive: true, force: true });
5723
+ for (const name of fs14.readdirSync(manifest.dir)) {
5724
+ const left = path14.join(manifest.dir, name);
5725
+ try {
5726
+ if (name.startsWith(TOUCH_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
5727
+ fs14.rmSync(left, { recursive: true, force: true });
5728
+ }
5729
+ } catch {
4951
5730
  }
4952
5731
  }
4953
- touchDir = fs12.mkdtempSync(path12.join(manifest.dir, TOUCH_PREFIX));
5732
+ touchDir = fs14.mkdtempSync(path14.join(manifest.dir, TOUCH_PREFIX));
4954
5733
  } catch {
4955
5734
  return { reason: "the subproject cannot be written to" };
4956
5735
  }
@@ -4961,14 +5740,14 @@ function shareOneRun(manifest, targets, runGate) {
4961
5740
  return { reason: "the gate run with every spec does not pass" };
4962
5741
  }
4963
5742
  const base = realPath(manifest.dir);
4964
- const absolute = (file) => realPath(path12.resolve(base, file));
5743
+ const absolute = (file) => realPath(path14.resolve(base, file));
4965
5744
  const rekeyed = (files) => Object.fromEntries(Object.entries(files).map(([file, value]) => [absolute(file), value]));
4966
5745
  const left = readNotes(touchDir);
4967
5746
  plan = planControls({
4968
5747
  all: manifest.gates.map((gate) => ({
4969
5748
  path: gate.path,
4970
- file: path12.join(base, gate.path),
4971
- specFile: path12.join(base, gate.spec ?? "")
5749
+ file: path14.join(base, gate.path),
5750
+ specFile: path14.join(base, gate.spec ?? "")
4972
5751
  })),
4973
5752
  targets: targets.map((gate) => gate.path),
4974
5753
  run: left.run,
@@ -4977,7 +5756,7 @@ function shareOneRun(manifest, targets, runGate) {
4977
5756
  failurePattern: manifest.settings.selfcheckFailurePattern
4978
5757
  });
4979
5758
  } finally {
4980
- fs12.rmSync(touchDir, { recursive: true, force: true });
5759
+ fs14.rmSync(touchDir, { recursive: true, force: true });
4981
5760
  }
4982
5761
  if (plan.reason !== null) {
4983
5762
  return { reason: plan.reason };
@@ -4987,15 +5766,15 @@ function shareOneRun(manifest, targets, runGate) {
4987
5766
  return { gates, result: runGate({ excludeAll: gates.map((gate) => gate.spec) }) };
4988
5767
  }
4989
5768
  function installedVersion2(start, name) {
4990
- let current = path12.resolve(start);
5769
+ let current = path14.resolve(start);
4991
5770
  for (; ; ) {
4992
5771
  try {
4993
5772
  const manifest = JSON.parse(
4994
- fs12.readFileSync(path12.join(current, "node_modules", name, "package.json"), "utf8")
5773
+ fs14.readFileSync(path14.join(current, "node_modules", name, "package.json"), "utf8")
4995
5774
  );
4996
5775
  return typeof manifest.version === "string" ? manifest.version : null;
4997
5776
  } catch {
4998
- const parent = path12.dirname(current);
5777
+ const parent = path14.dirname(current);
4999
5778
  if (parent === current) {
5000
5779
  return null;
5001
5780
  }
@@ -5024,24 +5803,39 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
5024
5803
  const results = /* @__PURE__ */ new Map();
5025
5804
  let root;
5026
5805
  try {
5027
- for (const name of fs12.readdirSync(os2.tmpdir())) {
5028
- const left = path12.join(os2.tmpdir(), name);
5029
- if (name.startsWith(JOBS_PREFIX) && Date.now() - fs12.statSync(left).mtimeMs > 36e5) {
5030
- fs12.rmSync(left, { recursive: true, force: true });
5806
+ root = fs14.realpathSync(fs14.mkdtempSync(path14.join(os2.tmpdir(), JOBS_PREFIX)));
5807
+ } catch {
5808
+ return results;
5809
+ }
5810
+ try {
5811
+ for (const name of fs14.readdirSync(os2.tmpdir())) {
5812
+ const left = path14.join(os2.tmpdir(), name);
5813
+ try {
5814
+ if (name.startsWith(JOBS_PREFIX) && Date.now() - fs14.statSync(left).mtimeMs > 36e5) {
5815
+ fs14.rmSync(left, { recursive: true, force: true });
5816
+ }
5817
+ } catch {
5031
5818
  }
5032
5819
  }
5033
- root = fs12.realpathSync(fs12.mkdtempSync(path12.join(os2.tmpdir(), JOBS_PREFIX)));
5034
5820
  } catch {
5035
- return results;
5036
5821
  }
5037
5822
  const queue = [...gates];
5823
+ const lines = Math.min(jobs, gates.length);
5824
+ const workers = workerFlags({
5825
+ runner,
5826
+ args,
5827
+ configSource: gateConfigSource(manifest, args),
5828
+ lines,
5829
+ // (os.availableParallelism is in Node 18.14 and later.)
5830
+ cores: os2.availableParallelism?.() ?? os2.cpus().length
5831
+ });
5038
5832
  const line = async (dir) => {
5039
5833
  for (let gate = queue.shift(); gate !== void 0; gate = queue.shift()) {
5040
5834
  const spec = gate.spec;
5041
5835
  results.set(
5042
5836
  gate,
5043
5837
  await new Promise((resolve) => {
5044
- const child = spawn(executable, [...args, ...jobFlags(runner, dir)], {
5838
+ const child = spawn(executable, [...args, ...jobFlags(runner, dir), ...workers], {
5045
5839
  cwd: manifest.dir,
5046
5840
  env: { ...process.env, [EXCLUDE_ENV]: spec, [EXCLUDE_ALL_ENV]: "", [TOUCH_ENV]: "" },
5047
5841
  shell: process.platform === "win32"
@@ -5060,13 +5854,10 @@ async function runAtOnce(manifest, gates, jobs, runner, executable, args) {
5060
5854
  };
5061
5855
  try {
5062
5856
  await Promise.all(
5063
- Array.from(
5064
- { length: Math.min(jobs, gates.length) },
5065
- (_unused, index) => line(path12.join(root, `line-${index}`))
5066
- )
5857
+ Array.from({ length: lines }, (_unused, index) => line(path14.join(root, `line-${index}`)))
5067
5858
  );
5068
5859
  } finally {
5069
- fs12.rmSync(root, { recursive: true, force: true });
5860
+ fs14.rmSync(root, { recursive: true, force: true });
5070
5861
  }
5071
5862
  return results;
5072
5863
  }
@@ -5095,7 +5886,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
5095
5886
  const command = commandOverride.length > 0 ? commandOverride : resolveGateCommand(manifest);
5096
5887
  const [program, ...args] = command;
5097
5888
  const executable = findLocalBin(manifest.dir, program) ?? program;
5098
- const runGate = (env) => spawnSync4(executable, args, {
5889
+ const runGate = (env) => spawnSync6(executable, args, {
5099
5890
  cwd: manifest.dir,
5100
5891
  env: {
5101
5892
  ...process.env,
@@ -5173,6 +5964,11 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
5173
5964
  ...manifest.gates.flatMap((gate) => [gate.path, gate.spec])
5174
5965
  ].sort((left, right) => right.length - left.length);
5175
5966
  const results = await runAtOnce(manifest, atOnce, jobs, known.runner, executable, args);
5967
+ if (results.size === 0) {
5968
+ io2.out(
5969
+ `test-gates selfcheck: one negative control at a time (no directory for the runs next to others could be made in ${os2.tmpdir()})`
5970
+ );
5971
+ }
5176
5972
  for (const [gate, result] of results) {
5177
5973
  const output = `${result.stdout}${result.stderr}`;
5178
5974
  const clean = cleanHold({
@@ -5190,7 +5986,7 @@ async function runSelfcheck(dir, selection, commandOverride, io2) {
5190
5986
  }
5191
5987
  }
5192
5988
  const again = atOnce.length - heldAtOnce.size;
5193
- if (again > 0) {
5989
+ if (again > 0 && results.size > 0) {
5194
5990
  io2.out(
5195
5991
  `test-gates selfcheck: ${again} of ${atOnce.length} negative control(s) did not hold cleanly next to others and run again on their own`
5196
5992
  );
@@ -5278,7 +6074,9 @@ Options:
5278
6074
  --affected mutation: run again only the gates that a change since the last
5279
6075
  run can affect, and reuse the results of the others. Every
5280
6076
  mutant is run when CI is set
5281
- --shard <index>/<total> selfcheck: one of <total> parts of the gates, for parallel CI jobs
6077
+ --shard <index>/<total> selfcheck, mutation: one of <total> parts of the gates, for parallel CI
6078
+ jobs. For mutation the gates are sorted and dealt out in turn; every
6079
+ part has to run. Accepted when CI is set
5282
6080
  --jobs <n> selfcheck: run <n> negative controls at the same time. One that does
5283
6081
  not hold cleanly there is run again on its own
5284
6082
  --quick selfcheck: one run for the gates only their own spec touches.
@@ -5313,7 +6111,7 @@ function main(argv) {
5313
6111
  case "doctor":
5314
6112
  return runDoctor(args.dir, io);
5315
6113
  case "mutation":
5316
- return runMutation(args.dir, args.rest, io, args.gates, args.affected);
6114
+ return runMutation(args.dir, args.rest, io, args.gates, args.affected, args.shard);
5317
6115
  case "mutation-result":
5318
6116
  return runMutationResult(args.dir, args.report, io);
5319
6117
  case "selfcheck":