@hublo/sentinel 1.4.0-alpha.31 → 1.4.0-alpha.33

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/README.md CHANGED
@@ -24,6 +24,7 @@ A large monorepo accumulates:
24
24
  - **One source of truth for config** — every project just `extends @hublo/sentinel/...`; the actual rules live in one versioned place. Change a rule once, everyone gets it on the next version bump.
25
25
  - **One source of truth for tooling dependencies** — a project depends on `@hublo/sentinel`, not on a scattered pile of eslint / vitest / plugin devDeps. Bump one version and the whole toolchain moves, atomically, tested in isolation first.
26
26
  - **`--init` sets a module up** — the one command generates the stubs the first time (**adopt**), regenerates them after a change like a runner swap (**refresh**), and applies the workspace prep the module needs. Run it module by module to roll out gradually. (`--migrate`, for changing an already-initialized setup, is a reserved future verb.)
27
+ - **`--validate` proves the move did not change anything** — a verb of its own, because what separates it from `--init` is what SURVIVES the run: `--init` and `--migrate` write state that REMAINS, `--validate` may instrument the module it is proving provided nothing it writes stays. Run before a migration it records what the module does today; run after, it compares and refuses a run that lost a test or invented one. It composes with every type, and answers "not available yet" for the ones without a proof rather than calling a valid command wrong.
27
28
  - **Move one module at a time** — installed per module, so you adopt at your pace; a module can adopt sentinel while its neighbour keeps the old setup. No big-bang.
28
29
  - **Swap tools without touching projects** — change eslint → biome (or benchmark them) in one place; `--init` regenerates the stubs.
29
30
  - **No silent drift** — the guard keeps every project's config converged on the source of truth.
@@ -15,10 +15,10 @@ import {
15
15
  palette,
16
16
  registerAdapters,
17
17
  resolve
18
- } from "../chunk-QYIF3HXC.js";
18
+ } from "../chunk-63NQP75B.js";
19
19
  import {
20
20
  resolveContext
21
- } from "../chunk-XM65P73B.js";
21
+ } from "../chunk-UBXL567Z.js";
22
22
  import "../chunk-O7REVMOC.js";
23
23
  import {
24
24
  ensureWorkspacePrep,
@@ -144,7 +144,7 @@ async function runValidate(ctx) {
144
144
  );
145
145
  }
146
146
  if (!ctx.targets.includes("test")) return 1;
147
- const { validateModuleTests } = await import("../validate-YRLGMILJ.js");
147
+ const { validateModuleTests } = await import("../validate-JUNITP37.js");
148
148
  const outcomes = modules.flatMap(
149
149
  (module) => validateModuleTests(module.name, module.root)
150
150
  );
@@ -27,7 +27,7 @@ import {
27
27
  selfCommand,
28
28
  splitToolArgs,
29
29
  walk
30
- } from "./chunk-XM65P73B.js";
30
+ } from "./chunk-UBXL567Z.js";
31
31
  import {
32
32
  DEFAULT_MAX_DIAGNOSTICS,
33
33
  findWorkspaceRoot,
@@ -723,7 +723,7 @@ function resolveVitest(cwd) {
723
723
 
724
724
  // src/roles/test/adapters/vitest/vitest.adapter.ts
725
725
  import { spawnSync } from "node:child_process";
726
- import { isAbsolute as isAbsolute2, resolve as resolve7 } from "node:path";
726
+ import { isAbsolute as isAbsolute2, resolve as resolve8 } from "node:path";
727
727
 
728
728
  // src/core/config/tool-args.ts
729
729
  import { existsSync as existsSync6 } from "node:fs";
@@ -873,9 +873,9 @@ function rewriteToolchainImports(toolchain, source, fileName) {
873
873
  }
874
874
 
875
875
  // src/roles/test/jest-migration.ts
876
- import { existsSync as existsSync18, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "node:fs";
876
+ import { existsSync as existsSync19, readFileSync as readFileSync18, readdirSync as readdirSync6 } from "node:fs";
877
877
  import { createRequire as createRequire5 } from "node:module";
878
- import { dirname as dirname10, join as join22, relative as relative5 } from "node:path";
878
+ import { dirname as dirname11, join as join23, relative as relative5 } from "node:path";
879
879
 
880
880
  // src/shared/jsonc.ts
881
881
  import { parse as parse2, printParseErrorCode } from "jsonc-parser";
@@ -890,9 +890,9 @@ function parseJsonc(text, source = "config") {
890
890
  }
891
891
 
892
892
  // src/roles/test/codemod/migrate-module.ts
893
- import { readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
894
- import { join as join15 } from "node:path";
895
- import { parseSync as parseSync7 } from "oxc-parser";
893
+ import { readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
894
+ import { join as join16 } from "node:path";
895
+ import { parseSync as parseSync8 } from "oxc-parser";
896
896
 
897
897
  // src/roles/test/msw-lifecycle.ts
898
898
  import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
@@ -1121,8 +1121,8 @@ ${source}`;
1121
1121
  }
1122
1122
 
1123
1123
  // src/roles/test/codemod/migrate-file.ts
1124
- import { dirname as dirname6 } from "node:path";
1125
- import { parseSync as parseSync6 } from "oxc-parser";
1124
+ import { dirname as dirname7 } from "node:path";
1125
+ import { parseSync as parseSync7 } from "oxc-parser";
1126
1126
 
1127
1127
  // src/roles/test/codemod/assertion-return-value.ts
1128
1128
  var MEMBER_TYPES = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
@@ -1295,6 +1295,20 @@ function assertedMockedClass(source, node) {
1295
1295
  if (object === void 0) return false;
1296
1296
  return /\bMockedClass\b/.test(source.slice(object.start, object.end));
1297
1297
  }
1298
+ function classValuedProperties(factories) {
1299
+ const found = [];
1300
+ for (const factory of factories) {
1301
+ walk(factory, (node) => {
1302
+ if (node.type !== "Property" && node.type !== "ObjectProperty") return;
1303
+ const key = node.key;
1304
+ const name = key?.type === "Identifier" ? key.name : key?.type === "StringLiteral" || key?.type === "Literal" ? String(key.value) : void 0;
1305
+ if (name === void 0 || !/^[A-Z]/.test(name)) return;
1306
+ const value = node.value;
1307
+ if (value !== void 0) found.push(value);
1308
+ });
1309
+ }
1310
+ return found;
1311
+ }
1298
1312
  function readsThis(node) {
1299
1313
  let found = false;
1300
1314
  walk(node, (inner) => {
@@ -1307,7 +1321,7 @@ function rewriteConstructibleMocks(source, program, factories) {
1307
1321
  const refusals = [];
1308
1322
  let rewritten = 0;
1309
1323
  const seen = /* @__PURE__ */ new Set();
1310
- const subtrees = [...factories];
1324
+ const subtrees = [...classValuedProperties(factories)];
1311
1325
  walk(program, (node) => {
1312
1326
  if (assertedMockedClass(source, node)) subtrees.push(node);
1313
1327
  });
@@ -1867,9 +1881,165 @@ function rewriteHookReturnValue(source, program) {
1867
1881
  }
1868
1882
 
1869
1883
  // src/roles/test/codemod/named-rules.ts
1870
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
1884
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
1871
1885
  import { createRequire as createRequire2 } from "node:module";
1872
- import { dirname as dirname2, join as join9 } from "node:path";
1886
+ import { dirname as dirname3, join as join10 } from "node:path";
1887
+
1888
+ // src/roles/test/codemod/static-import-for-require.ts
1889
+ import { existsSync as existsSync10, readFileSync as readFileSync7, statSync } from "node:fs";
1890
+ import { dirname as dirname2, join as join9, resolve as resolve3 } from "node:path";
1891
+ import { parseSync as parseSync4 } from "oxc-parser";
1892
+ var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1893
+ function resolveRelative(fromFile, specifier) {
1894
+ const base = resolve3(dirname2(fromFile), specifier);
1895
+ const candidates = [
1896
+ base,
1897
+ ...EXTENSIONS.map((extension) => `${base}${extension}`),
1898
+ ...EXTENSIONS.map((extension) => join9(base, `index${extension}`))
1899
+ ];
1900
+ for (const candidate of candidates) {
1901
+ try {
1902
+ if (statSync(candidate).isFile()) return candidate;
1903
+ } catch {
1904
+ }
1905
+ }
1906
+ return void 0;
1907
+ }
1908
+ function relativeSpecifiers(source) {
1909
+ const found = [];
1910
+ const patterns = [
1911
+ /\bfrom\s+['"](\.[^'"]*)['"]/g,
1912
+ /\bimport\s+['"](\.[^'"]*)['"]/g,
1913
+ /\bimport\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g,
1914
+ /\brequire\s*\(\s*['"](\.[^'"]*)['"]\s*\)/g
1915
+ ];
1916
+ for (const pattern of patterns) {
1917
+ for (const match of source.matchAll(pattern)) found.push(match[1]);
1918
+ }
1919
+ return found;
1920
+ }
1921
+ function reachesBack(fromFile, specifier) {
1922
+ const entry = resolveRelative(fromFile, specifier);
1923
+ if (entry === void 0) return true;
1924
+ const target = resolve3(fromFile);
1925
+ const seen = /* @__PURE__ */ new Set();
1926
+ const stack = [entry];
1927
+ while (stack.length > 0 && seen.size < 200) {
1928
+ const file = stack.pop();
1929
+ if (seen.has(file)) continue;
1930
+ seen.add(file);
1931
+ if (resolve3(file) === target) return true;
1932
+ let source;
1933
+ try {
1934
+ source = readFileSync7(file, "utf8");
1935
+ } catch {
1936
+ continue;
1937
+ }
1938
+ for (const relative6 of relativeSpecifiers(source)) {
1939
+ const next = resolveRelative(file, relative6);
1940
+ if (next !== void 0) stack.push(next);
1941
+ }
1942
+ }
1943
+ return seen.size >= 200;
1944
+ }
1945
+ function namedRequireDeclarations(program) {
1946
+ const found = /* @__PURE__ */ new Map();
1947
+ walk(program, (node) => {
1948
+ if (node.type !== "VariableDeclaration") return;
1949
+ const declarators = node.declarations ?? [];
1950
+ if (declarators.length !== 1) return;
1951
+ const declarator = declarators[0];
1952
+ const init = declarator.init;
1953
+ if (init?.type !== "CallExpression") return;
1954
+ const callee = init.callee;
1955
+ if (callee?.type !== "Identifier" || callee.name !== "require") return;
1956
+ const [argument] = init.arguments ?? [];
1957
+ if (argument?.type !== "StringLiteral" && argument?.type !== "Literal") return;
1958
+ const specifier = String(argument.value);
1959
+ if (!specifier.startsWith(".")) return;
1960
+ const id = declarator.id;
1961
+ if (id?.type !== "ObjectPattern") return;
1962
+ const names = [];
1963
+ for (const property of id.properties ?? []) {
1964
+ if (property.type !== "Property" && property.type !== "ObjectProperty") return;
1965
+ if (property.computed === true || property.shorthand !== true) return;
1966
+ const key = property.key;
1967
+ const value = property.value;
1968
+ if (key?.type !== "Identifier" || value?.type !== "Identifier") return;
1969
+ if (key.name !== value.name) return;
1970
+ names.push(key.name);
1971
+ }
1972
+ if (names.length === 0) return;
1973
+ found.set(init, { names, specifier, start: node.start, end: node.end });
1974
+ });
1975
+ return found;
1976
+ }
1977
+ function topLevelNames(program) {
1978
+ const names = /* @__PURE__ */ new Set();
1979
+ for (const node of program.body ?? []) {
1980
+ if (node.type === "ImportDeclaration") {
1981
+ for (const specifier of node.specifiers ?? []) {
1982
+ const local = specifier.local;
1983
+ if (local?.type === "Identifier") names.add(local.name);
1984
+ }
1985
+ continue;
1986
+ }
1987
+ if (node.type === "VariableDeclaration") {
1988
+ for (const declarator of node.declarations ?? []) {
1989
+ const id = declarator.id;
1990
+ if (id?.type === "Identifier") names.add(id.name);
1991
+ }
1992
+ continue;
1993
+ }
1994
+ if ((node.type === "FunctionDeclaration" || node.type === "ClassDeclaration") && node.id?.type === "Identifier") {
1995
+ names.add(node.id.name);
1996
+ }
1997
+ }
1998
+ return names;
1999
+ }
2000
+ function withLeadingComments(source, comments, start) {
2001
+ let from = start;
2002
+ for (; ; ) {
2003
+ const before = source.slice(0, from);
2004
+ const blank = before.length - before.trimEnd().length;
2005
+ const candidate = comments.find((comment) => comment.end === from - blank);
2006
+ if (candidate === void 0) break;
2007
+ const lineStart = source.lastIndexOf("\n", candidate.start) + 1;
2008
+ if (source.slice(lineStart, candidate.start).trim() !== "") break;
2009
+ from = lineStart;
2010
+ }
2011
+ return from;
2012
+ }
2013
+ function planStaticImport(fileName, source, program, requireCall2, semi) {
2014
+ const declaration = namedRequireDeclarations(program).get(requireCall2);
2015
+ if (declaration === void 0) return void 0;
2016
+ const bound = topLevelNames(program);
2017
+ if (declaration.names.some((name) => bound.has(name))) return void 0;
2018
+ if (reachesBack(fileName, declaration.specifier)) return void 0;
2019
+ const { comments } = parseSync4(fileName, source, { sourceType: "module" });
2020
+ const lineStart = source.lastIndexOf("\n", declaration.start - 1) + 1;
2021
+ const ownsItsLine = source.slice(lineStart, declaration.start).trim() === "";
2022
+ const start = withLeadingComments(
2023
+ source,
2024
+ comments,
2025
+ ownsItsLine ? lineStart : declaration.start
2026
+ );
2027
+ return {
2028
+ line: `import { ${declaration.names.join(", ")} } from '${declaration.specifier}'${semi ? ";" : ""}`,
2029
+ // To the end of the line, so the statement does not leave an empty one behind.
2030
+ remove: { start, end: endOfLine(source, declaration.end) }
2031
+ };
2032
+ }
2033
+ function endOfLine(source, end) {
2034
+ let at = end;
2035
+ if (source[at] === ";") at += 1;
2036
+ while (source[at] === " " || source[at] === " ") at += 1;
2037
+ if (source[at] === "\r") at += 1;
2038
+ if (source[at] === "\n") at += 1;
2039
+ return at;
2040
+ }
2041
+
2042
+ // src/roles/test/codemod/named-rules.ts
1873
2043
  function isCommonJsPackage(fromFile, specifier) {
1874
2044
  if (specifier.startsWith(".") || specifier.startsWith("/")) return false;
1875
2045
  let resolved;
@@ -1881,23 +2051,27 @@ function isCommonJsPackage(fromFile, specifier) {
1881
2051
  if (!resolved.includes(`${"/"}node_modules${"/"}`)) return false;
1882
2052
  if (resolved.endsWith(".mjs")) return false;
1883
2053
  if (resolved.endsWith(".cjs")) return true;
1884
- let dir = dirname2(resolved);
2054
+ let dir = dirname3(resolved);
1885
2055
  for (let depth = 0; depth < 12; depth += 1) {
1886
- const manifest = join9(dir, "package.json");
1887
- if (existsSync10(manifest)) {
2056
+ const manifest = join10(dir, "package.json");
2057
+ if (existsSync11(manifest)) {
1888
2058
  try {
1889
- return JSON.parse(readFileSync7(manifest, "utf8")).type !== "module";
2059
+ return JSON.parse(readFileSync8(manifest, "utf8")).type !== "module";
1890
2060
  } catch {
1891
2061
  return false;
1892
2062
  }
1893
2063
  }
1894
- const parent = dirname2(dir);
2064
+ const parent = dirname3(dir);
1895
2065
  if (parent === dir) break;
1896
2066
  dir = parent;
1897
2067
  }
1898
2068
  return false;
1899
2069
  }
1900
2070
  var MEMBER_TYPES9 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2071
+ function fileUsesSemicolons(source) {
2072
+ const first = /^import\s[^\n]*$/m.exec(source);
2073
+ return first !== null && first[0].trimEnd().endsWith(";");
2074
+ }
1901
2075
  var ASYNC_REPLACEMENTS = {
1902
2076
  requireActual: "importActual",
1903
2077
  requireMock: "importMock"
@@ -1920,6 +2094,16 @@ function mockedSpecifiers(program) {
1920
2094
  });
1921
2095
  return found;
1922
2096
  }
2097
+ function methodValues(program) {
2098
+ const found = /* @__PURE__ */ new Set();
2099
+ walk(program, (node) => {
2100
+ const isMethod = node.type === "MethodDefinition" || node.type === "ClassMethod" || node.type === "ObjectMethod" || (node.type === "Property" || node.type === "ObjectProperty") && (node.kind === "get" || node.kind === "set" || node.method === true);
2101
+ if (!isMethod) return;
2102
+ const value = node.value ?? node.body;
2103
+ if (value !== void 0) found.add(value);
2104
+ });
2105
+ return found;
2106
+ }
1923
2107
  var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
1924
2108
  function walkWithEnclosingFunction(node, enclosing, visit) {
1925
2109
  if (node === null || typeof node !== "object") return;
@@ -2002,7 +2186,25 @@ function applyNamedRules(source, program, fileName = "") {
2002
2186
  const edits = [];
2003
2187
  const applied = /* @__PURE__ */ new Set();
2004
2188
  const asyncify = /* @__PURE__ */ new Set();
2189
+ const refusals = [];
2190
+ const staticImports = [];
2005
2191
  const mocked = mockedSpecifiers(program);
2192
+ const methods = methodValues(program);
2193
+ const cannotAwaitHere = (enclosing, what, call) => {
2194
+ if (enclosing === void 0 || !methods.has(enclosing)) return false;
2195
+ const plan2 = planStaticImport(fileName, source, program, call, fileUsesSemicolons(source));
2196
+ if (plan2 !== void 0) {
2197
+ edits.push({ start: plan2.remove.start, end: plan2.remove.end, text: "" });
2198
+ staticImports.push(plan2.line);
2199
+ applied.add(`${what} as a static import`);
2200
+ return true;
2201
+ }
2202
+ refusals.push({
2203
+ binding: what,
2204
+ reason: `\`${what}\` sits inside a class or object METHOD, which cannot become async here: \`async get\` is not valid, and making a method that returns void return a promise changes what every caller gets. Left as it was. Under Vitest \`require\` is Node's own resolver, which knows nothing of \`.ts\`, so this one needs a person: a static import, or the dynamic one kept on purpose.`
2205
+ });
2206
+ return true;
2207
+ };
2006
2208
  const wholeStatement = /* @__PURE__ */ new Map();
2007
2209
  walk(program, (node) => {
2008
2210
  if (node.type !== "ExpressionStatement") return;
@@ -2070,6 +2272,7 @@ function applyNamedRules(source, program, fileName = "") {
2070
2272
  const [specifier] = node.arguments ?? [];
2071
2273
  const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
2072
2274
  if (literal !== void 0 && mocked.has(literal)) {
2275
+ if (cannotAwaitHere(enclosing, "jest.requireMock", node)) return;
2073
2276
  edits.push({
2074
2277
  start: node.start,
2075
2278
  end: node.end,
@@ -2082,6 +2285,7 @@ function applyNamedRules(source, program, fileName = "") {
2082
2285
  }
2083
2286
  const asyncReplacement = member === void 0 ? void 0 : ASYNC_REPLACEMENTS[member];
2084
2287
  if (asyncReplacement !== void 0) {
2288
+ if (cannotAwaitHere(enclosing, `jest.${member}`, node)) return;
2085
2289
  edits.push({
2086
2290
  start: node.start,
2087
2291
  end: node.end,
@@ -2095,6 +2299,7 @@ function applyNamedRules(source, program, fileName = "") {
2095
2299
  return;
2096
2300
  }
2097
2301
  if (bareRequire(node)) {
2302
+ if (cannotAwaitHere(enclosing, "require", node)) return;
2098
2303
  const [specifier] = node.arguments ?? [];
2099
2304
  const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
2100
2305
  const interop = literal !== void 0 && !readsANameOffIt.has(node) && !handedToTheRunnerAsAModule.has(node) && !wholeStatement.has(node) && isCommonJsPackage(fileName, literal) ? ".default" : "";
@@ -2129,7 +2334,27 @@ function applyNamedRules(source, program, fileName = "") {
2129
2334
  for (const fn of asyncify) {
2130
2335
  edits.push({ start: fn.start, end: fn.start, text: "async " });
2131
2336
  }
2132
- return { source: applyEdits(source, edits), applied: [...applied].sort() };
2337
+ if (staticImports.length > 0) {
2338
+ const first = (program.body ?? []).find(
2339
+ (node) => node.type === "ImportDeclaration"
2340
+ );
2341
+ const byModule = /* @__PURE__ */ new Map();
2342
+ for (const line of staticImports) {
2343
+ const parsed = /^import \{ (.+) \} from '(.+)'(;?)$/.exec(line);
2344
+ if (parsed === null) continue;
2345
+ const names = byModule.get(parsed[2]) ?? /* @__PURE__ */ new Set();
2346
+ for (const name of parsed[1].split(", ")) names.add(name);
2347
+ byModule.set(parsed[2], names);
2348
+ }
2349
+ const semi = staticImports[0]?.endsWith(";") === true ? ";" : "";
2350
+ const block = `${[...byModule].map(([specifier, names]) => `import { ${[...names].join(", ")} } from '${specifier}'${semi}`).join("\n")}
2351
+ `;
2352
+ edits.push(
2353
+ first === void 0 ? { start: 0, end: 0, text: `${block}
2354
+ ` } : { start: first.start, end: first.start, text: block }
2355
+ );
2356
+ }
2357
+ return { source: applyEdits(source, edits), applied: [...applied].sort(), refusals };
2133
2358
  }
2134
2359
 
2135
2360
  // src/roles/test/codemod/isolate-modules.ts
@@ -2525,7 +2750,7 @@ function annotateMockedBindings(source, program) {
2525
2750
  }
2526
2751
 
2527
2752
  // src/roles/test/codemod/propagate-await.ts
2528
- import { parseSync as parseSync4 } from "oxc-parser";
2753
+ import { parseSync as parseSync5 } from "oxc-parser";
2529
2754
  var FUNCTIONS3 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"]);
2530
2755
  var MEMBER_TYPES14 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2531
2756
  var ALREADY_A_PROMISE = /* @__PURE__ */ new Set(["then", "catch", "finally"]);
@@ -2621,7 +2846,7 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
2621
2846
  for (let round = 0; round < 12; round += 1) {
2622
2847
  let parsed;
2623
2848
  try {
2624
- parsed = parseSync4(fileName, current);
2849
+ parsed = parseSync5(fileName, current);
2625
2850
  } catch {
2626
2851
  return { source: current, rewritten };
2627
2852
  }
@@ -2651,9 +2876,9 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
2651
2876
  }
2652
2877
 
2653
2878
  // src/roles/test/codemod/reexport-barrel-mock.ts
2654
- import { readFileSync as readFileSync8, readdirSync as readdirSync2, statSync } from "node:fs";
2879
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
2655
2880
  import { createRequire as createRequire3 } from "node:module";
2656
- import { dirname as dirname3, join as join10 } from "node:path";
2881
+ import { dirname as dirname4, join as join11 } from "node:path";
2657
2882
  var MEMBER_TYPES15 = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
2658
2883
  var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
2659
2884
  var PURE_REEXPORT = /^module\.exports\s*=\s*require\(\s*(['"])([^'"]+)\1\s*\)\s*;?$/;
@@ -2682,7 +2907,7 @@ function mockCalls(program) {
2682
2907
  }
2683
2908
  function packageRoot(fromFile, name) {
2684
2909
  try {
2685
- return dirname3(createRequire3(fromFile).resolve(`${name}/package.json`));
2910
+ return dirname4(createRequire3(fromFile).resolve(`${name}/package.json`));
2686
2911
  } catch {
2687
2912
  return void 0;
2688
2913
  }
@@ -2706,11 +2931,11 @@ function barrelsFor(fromFile, name, target) {
2706
2931
  const aliases = [];
2707
2932
  for (const entry of entries) {
2708
2933
  if (!entry.endsWith(".js")) continue;
2709
- const path = join10(root, entry);
2934
+ const path = join11(root, entry);
2710
2935
  let body;
2711
2936
  try {
2712
- if (!statSync(path).isFile()) continue;
2713
- body = readFileSync8(path, "utf8");
2937
+ if (!statSync2(path).isFile()) continue;
2938
+ body = readFileSync9(path, "utf8");
2714
2939
  } catch {
2715
2940
  continue;
2716
2941
  }
@@ -2750,7 +2975,7 @@ ${text}` });
2750
2975
  }
2751
2976
 
2752
2977
  // src/roles/test/codemod/repoint-msw.ts
2753
- import { parseSync as parseSync5 } from "oxc-parser";
2978
+ import { parseSync as parseSync6 } from "oxc-parser";
2754
2979
  var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
2755
2980
  var SENTINEL_MSW_API = "@hublo/sentinel/test/tools/msw";
2756
2981
  var MSW_PACKAGE = /^msw$/;
@@ -2763,7 +2988,7 @@ function importsServer(declaration) {
2763
2988
  });
2764
2989
  }
2765
2990
  function repointMswServer(fileName, source) {
2766
- const { program, errors } = parseSync5(fileName, source, { sourceType: "module" });
2991
+ const { program, errors } = parseSync6(fileName, source, { sourceType: "module" });
2767
2992
  if (errors.length > 0) return { source, repointed: false };
2768
2993
  const edits = [];
2769
2994
  walk(program, (node) => {
@@ -2789,17 +3014,17 @@ function repointMswServer(fileName, source) {
2789
3014
  }
2790
3015
 
2791
3016
  // src/roles/test/codemod/setup-static-imports.ts
2792
- import { dirname as dirname5, resolve as resolve5 } from "node:path";
3017
+ import { dirname as dirname6, resolve as resolve6 } from "node:path";
2793
3018
 
2794
3019
  // src/core/context.ts
2795
- import { existsSync as existsSync11 } from "node:fs";
2796
- import { dirname as dirname4, join as join12, resolve as resolve3 } from "node:path";
3020
+ import { existsSync as existsSync12 } from "node:fs";
3021
+ import { dirname as dirname5, join as join13, resolve as resolve4 } from "node:path";
2797
3022
 
2798
3023
  // src/core/discover-modules.ts
2799
3024
  import { execFileSync as execFileSync2 } from "node:child_process";
2800
- import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, rmSync as rmSync2 } from "node:fs";
3025
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "node:fs";
2801
3026
  import { tmpdir as tmpdir2 } from "node:os";
2802
- import { join as join11 } from "node:path";
3027
+ import { join as join12 } from "node:path";
2803
3028
  function runNx(cwd, args) {
2804
3029
  const nx = resolveBin(cwd, "nx") ?? "nx";
2805
3030
  try {
@@ -2817,11 +3042,11 @@ function runNx(cwd, args) {
2817
3042
  }
2818
3043
  }
2819
3044
  function readGraph(cwd) {
2820
- const dir = mkdtempSync2(join11(tmpdir2(), "sentinel-nx-"));
2821
- const file = join11(dir, "graph.json");
3045
+ const dir = mkdtempSync2(join12(tmpdir2(), "sentinel-nx-"));
3046
+ const file = join12(dir, "graph.json");
2822
3047
  try {
2823
3048
  runNx(cwd, ["graph", "--file", file]);
2824
- const parsed = JSON.parse(readFileSync9(file, "utf8"));
3049
+ const parsed = JSON.parse(readFileSync10(file, "utf8"));
2825
3050
  const nodes = parsed.graph?.nodes;
2826
3051
  if (!nodes || typeof nodes !== "object") {
2827
3052
  throw new Error(
@@ -2830,7 +3055,7 @@ function readGraph(cwd) {
2830
3055
  }
2831
3056
  return Object.entries(nodes).map(([name, node]) => ({
2832
3057
  name,
2833
- root: join11(cwd, node.data.root)
3058
+ root: join12(cwd, node.data.root)
2834
3059
  }));
2835
3060
  } finally {
2836
3061
  rmSync2(dir, { recursive: true, force: true });
@@ -2848,20 +3073,20 @@ function discoverModules(cwd, options = {}) {
2848
3073
  // src/core/context.ts
2849
3074
  var MODULE_MARKERS = ["package.json", "project.json"];
2850
3075
  function isModuleDir(cwd) {
2851
- return MODULE_MARKERS.some((marker) => existsSync11(join12(cwd, marker)));
3076
+ return MODULE_MARKERS.some((marker) => existsSync12(join13(cwd, marker)));
2852
3077
  }
2853
3078
  function findModuleRoot(startDir, workspaceRoot) {
2854
- let dir = resolve3(startDir);
3079
+ let dir = resolve4(startDir);
2855
3080
  for (; ; ) {
2856
- if (workspaceRoot !== void 0 && dir === resolve3(workspaceRoot)) return void 0;
3081
+ if (workspaceRoot !== void 0 && dir === resolve4(workspaceRoot)) return void 0;
2857
3082
  if (isModuleDir(dir)) return dir;
2858
- const parent = dirname4(dir);
3083
+ const parent = dirname5(dir);
2859
3084
  if (parent === dir) return void 0;
2860
3085
  dir = parent;
2861
3086
  }
2862
3087
  }
2863
3088
  function resolveContext(cwd, opts) {
2864
- const atRoot = existsSync11(join12(cwd, WORKSPACE_ROOT_MARKER));
3089
+ const atRoot = existsSync12(join13(cwd, WORKSPACE_ROOT_MARKER));
2865
3090
  if (!atRoot && isModuleDir(cwd)) {
2866
3091
  if (opts.module) {
2867
3092
  throw new Error(
@@ -2889,16 +3114,16 @@ function resolveContext(cwd, opts) {
2889
3114
  }
2890
3115
 
2891
3116
  // src/roles/test/codemod/workspace-specifier.ts
2892
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
2893
- import { join as join13, relative as relative2, resolve as resolve4 } from "node:path";
2894
- var EXTENSIONS = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.js"];
3117
+ import { existsSync as existsSync13, readFileSync as readFileSync11 } from "node:fs";
3118
+ import { join as join14, relative as relative2, resolve as resolve5 } from "node:path";
3119
+ var EXTENSIONS2 = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.js"];
2895
3120
  function pathEntries(workspaceRoot) {
2896
- const file = join13(workspaceRoot, "tsconfig.base.json");
2897
- if (!existsSync12(file)) return [];
3121
+ const file = join14(workspaceRoot, "tsconfig.base.json");
3122
+ if (!existsSync13(file)) return [];
2898
3123
  let paths;
2899
3124
  try {
2900
3125
  paths = parseJsonc(
2901
- readFileSync10(file, "utf8"),
3126
+ readFileSync11(file, "utf8"),
2902
3127
  "tsconfig.base.json"
2903
3128
  )?.compilerOptions?.paths;
2904
3129
  } catch {
@@ -2911,17 +3136,17 @@ function pathEntries(workspaceRoot) {
2911
3136
  const wildcard = key.endsWith("/*") && target.endsWith("/*");
2912
3137
  entries.push({
2913
3138
  key: wildcard ? key.slice(0, -2) : key,
2914
- target: resolve4(workspaceRoot, wildcard ? target.slice(0, -2) : target),
3139
+ target: resolve5(workspaceRoot, wildcard ? target.slice(0, -2) : target),
2915
3140
  wildcard
2916
3141
  });
2917
3142
  }
2918
3143
  return entries.sort((left, right) => right.target.length - left.target.length);
2919
3144
  }
2920
3145
  function workspaceSpecifier(workspaceRoot, file) {
2921
- const absolute = resolve4(file);
3146
+ const absolute = resolve5(file);
2922
3147
  for (const entry of pathEntries(workspaceRoot)) {
2923
3148
  if (!entry.wildcard) {
2924
- if (EXTENSIONS.some((extension) => `${entry.target}${extension}` === absolute)) {
3149
+ if (EXTENSIONS2.some((extension) => `${entry.target}${extension}` === absolute)) {
2925
3150
  return entry.key;
2926
3151
  }
2927
3152
  continue;
@@ -2947,9 +3172,9 @@ function requireCall(source, node) {
2947
3172
  }
2948
3173
  function importable(written, fileName, workspaceRoot) {
2949
3174
  if (!written.startsWith(".")) return written;
2950
- const fileDir = dirname5(fileName);
3175
+ const fileDir = dirname6(fileName);
2951
3176
  const moduleRoot = findModuleRoot(fileDir, workspaceRoot) ?? fileDir;
2952
- const target = resolve5(fileDir, written);
3177
+ const target = resolve6(fileDir, written);
2953
3178
  if (target.startsWith(`${moduleRoot}/`)) return written;
2954
3179
  if (workspaceRoot === void 0) return void 0;
2955
3180
  return workspaceSpecifier(workspaceRoot, target);
@@ -3090,7 +3315,7 @@ function mockFactories(program) {
3090
3315
  return factories;
3091
3316
  }
3092
3317
  function parse3(fileName, source) {
3093
- const { program, errors } = parseSync6(fileName, source, { sourceType: "module" });
3318
+ const { program, errors } = parseSync7(fileName, source, { sourceType: "module" });
3094
3319
  return errors.length > 0 ? void 0 : program;
3095
3320
  }
3096
3321
  function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new Set(), asyncHelpers = /* @__PURE__ */ new Set(), modulesOwnMsw = false) {
@@ -3120,7 +3345,7 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
3120
3345
  fileName,
3121
3346
  source,
3122
3347
  beforeStatic,
3123
- findWorkspaceRoot(dirname6(fileName))
3348
+ findWorkspaceRoot(dirname7(fileName))
3124
3349
  );
3125
3350
  source = statics.source;
3126
3351
  const original = statics.rewritten === 0 ? beforeStatic : parse3(fileName, source);
@@ -3235,7 +3460,8 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
3235
3460
  ...hoist.refusals,
3236
3461
  ...mocks.refusals.map((refusal) => ({ binding: refusal.binding, reason: refusal.reason })),
3237
3462
  ...constructible.refusals,
3238
- ...isolated.refusals
3463
+ ...isolated.refusals,
3464
+ ...ruled.refusals
3239
3465
  ],
3240
3466
  warnings: fakeTimers.warnings,
3241
3467
  mswRepointed: repointed.repointed
@@ -3243,13 +3469,13 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
3243
3469
  }
3244
3470
 
3245
3471
  // src/roles/test/codemod/test-support-files.ts
3246
- import { existsSync as existsSync13, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
3247
- import { dirname as dirname7, join as join14, relative as relative3, resolve as resolve6 } from "node:path";
3472
+ import { existsSync as existsSync14, readFileSync as readFileSync12, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
3473
+ import { dirname as dirname8, join as join15, relative as relative3, resolve as resolve7 } from "node:path";
3248
3474
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
3249
3475
  var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
3250
3476
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
3251
- var EXTENSIONS2 = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
3252
- function relativeSpecifiers(source) {
3477
+ var EXTENSIONS3 = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
3478
+ function relativeSpecifiers2(source) {
3253
3479
  const found = [];
3254
3480
  const patterns = [
3255
3481
  /\bfrom\s+['"](\.[^'"]*)['"]/g,
@@ -3263,14 +3489,14 @@ function relativeSpecifiers(source) {
3263
3489
  return found;
3264
3490
  }
3265
3491
  function resolveSpecifier(fromFile, specifier) {
3266
- const base = resolve6(dirname7(fromFile), specifier);
3267
- if (existsSync13(base) && statSync2(base).isFile()) return base;
3268
- for (const extension of EXTENSIONS2) {
3269
- if (existsSync13(`${base}${extension}`)) return `${base}${extension}`;
3492
+ const base = resolve7(dirname8(fromFile), specifier);
3493
+ if (existsSync14(base) && statSync3(base).isFile()) return base;
3494
+ for (const extension of EXTENSIONS3) {
3495
+ if (existsSync14(`${base}${extension}`)) return `${base}${extension}`;
3270
3496
  }
3271
- for (const extension of EXTENSIONS2) {
3272
- const index = join14(base, `index${extension}`);
3273
- if (existsSync13(index)) return index;
3497
+ for (const extension of EXTENSIONS3) {
3498
+ const index = join15(base, `index${extension}`);
3499
+ if (existsSync14(index)) return index;
3274
3500
  }
3275
3501
  return void 0;
3276
3502
  }
@@ -3287,9 +3513,9 @@ function moduleSources(cwd) {
3287
3513
  }
3288
3514
  for (const entry of entries) {
3289
3515
  if (entry.isDirectory()) {
3290
- if (!SKIPPED.has(entry.name)) stack.push(join14(dir, entry.name));
3516
+ if (!SKIPPED.has(entry.name)) stack.push(join15(dir, entry.name));
3291
3517
  } else if (SOURCE_FILE.test(entry.name)) {
3292
- found.push(join14(dir, entry.name));
3518
+ found.push(join15(dir, entry.name));
3293
3519
  }
3294
3520
  }
3295
3521
  }
@@ -3297,7 +3523,7 @@ function moduleSources(cwd) {
3297
3523
  }
3298
3524
  function read(path) {
3299
3525
  try {
3300
- return readFileSync11(path, "utf8");
3526
+ return readFileSync12(path, "utf8");
3301
3527
  } catch {
3302
3528
  return void 0;
3303
3529
  }
@@ -3306,10 +3532,10 @@ function setupFilesInside(cwd, named) {
3306
3532
  const kept = [];
3307
3533
  for (const raw of named) {
3308
3534
  const withoutToken = raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", ".");
3309
- const full = resolve6(cwd, withoutToken);
3535
+ const full = resolve7(cwd, withoutToken);
3310
3536
  const inside = relative3(cwd, full);
3311
3537
  if (inside.startsWith("..") || inside === "") continue;
3312
- if (!existsSync13(full)) continue;
3538
+ if (!existsSync14(full)) continue;
3313
3539
  kept.push(inside.replaceAll("\\", "/"));
3314
3540
  }
3315
3541
  return kept;
@@ -3323,7 +3549,7 @@ function listTestSupportFiles(cwd, setupFiles = []) {
3323
3549
  }
3324
3550
  const importedBy = /* @__PURE__ */ new Map();
3325
3551
  for (const [file, source] of contents) {
3326
- for (const specifier of relativeSpecifiers(source)) {
3552
+ for (const specifier of relativeSpecifiers2(source)) {
3327
3553
  const target = resolveSpecifier(file, specifier);
3328
3554
  if (target === void 0 || !contents.has(target)) continue;
3329
3555
  importedBy.set(target, [...importedBy.get(target) ?? [], file]);
@@ -3336,7 +3562,7 @@ function listTestSupportFiles(cwd, setupFiles = []) {
3336
3562
  );
3337
3563
  while (queue.length > 0) {
3338
3564
  const file = queue.shift();
3339
- for (const specifier of relativeSpecifiers(contents.get(file) ?? "")) {
3565
+ for (const specifier of relativeSpecifiers2(contents.get(file) ?? "")) {
3340
3566
  const target = resolveSpecifier(file, specifier);
3341
3567
  if (target === void 0 || TEST_FILE2.test(target) || testOnly.has(target)) continue;
3342
3568
  if (!isTestOnly(target)) continue;
@@ -3350,12 +3576,12 @@ function listTestSupportFiles(cwd, setupFiles = []) {
3350
3576
 
3351
3577
  // src/roles/test/codemod/migrate-module.ts
3352
3578
  function parse4(source) {
3353
- const { program, errors } = parseSync7("file.ts", source, { sourceType: "module" });
3579
+ const { program, errors } = parseSync8("file.ts", source, { sourceType: "module" });
3354
3580
  return errors.length > 0 ? void 0 : program;
3355
3581
  }
3356
3582
  function read2(path) {
3357
3583
  try {
3358
- return readFileSync12(path, "utf8");
3584
+ return readFileSync13(path, "utf8");
3359
3585
  } catch {
3360
3586
  return void 0;
3361
3587
  }
@@ -3373,10 +3599,10 @@ function defaultImportedSpecifiers(cwd) {
3373
3599
  }
3374
3600
  for (const entry of entries) {
3375
3601
  if (SKIPPED2.has(entry)) continue;
3376
- const path = join15(dir, entry);
3602
+ const path = join16(dir, entry);
3377
3603
  let directory;
3378
3604
  try {
3379
- directory = statSync3(path).isDirectory();
3605
+ directory = statSync4(path).isDirectory();
3380
3606
  } catch {
3381
3607
  continue;
3382
3608
  }
@@ -3404,18 +3630,28 @@ function migrateModuleTests(cwd, setupFiles = []) {
3404
3630
  const defaultImported = defaultImportedSpecifiers(cwd);
3405
3631
  const asyncHelpers = /* @__PURE__ */ new Set();
3406
3632
  for (const relative6 of listTestSupportFiles(cwd, setupFiles)) {
3407
- const source = read2(join15(cwd, relative6));
3633
+ const source = read2(join16(cwd, relative6));
3408
3634
  if (source === void 0) {
3409
3635
  blockers.push({ file: relative6, reason: "this file could not be read" });
3410
3636
  continue;
3411
3637
  }
3412
3638
  const migrated = migrateTestFile(
3413
- join15(cwd, relative6),
3639
+ join16(cwd, relative6),
3414
3640
  source,
3415
3641
  defaultImported,
3416
3642
  /* @__PURE__ */ new Set(),
3417
3643
  modulesOwnMsw
3418
3644
  );
3645
+ for (const member of migrated.unhandled) {
3646
+ blockers.push({
3647
+ file: `${relative6}:${member.line}`,
3648
+ reason: member.rule === void 0 ? `${member.member} has no rule, and guessing one is how a suite goes green for the wrong reason` : `${member.member} needs ${member.rule}`
3649
+ });
3650
+ }
3651
+ for (const refusal of migrated.refusals) {
3652
+ blockers.push({ file: relative6, reason: refusal.reason });
3653
+ }
3654
+ collected.push(...migrated.warnings);
3419
3655
  const renamedTo = vitestSupportName(relative6);
3420
3656
  if (!migrated.changed && renamedTo === relative6) continue;
3421
3657
  if (renamedTo !== relative6) operations.push({ kind: "delete", path: relative6 });
@@ -3429,17 +3665,17 @@ function migrateModuleTests(cwd, setupFiles = []) {
3429
3665
  }
3430
3666
  files++;
3431
3667
  renamed += migrated.renamed;
3432
- const contents = /\bvi\./.test(migrated.source) ? injectViImport(join15(cwd, renamedTo), migrated.source) : migrated.source;
3668
+ const contents = /\bvi\./.test(migrated.source) ? injectViImport(join16(cwd, renamedTo), migrated.source) : migrated.source;
3433
3669
  operations.push({ kind: "write", path: renamedTo, contents });
3434
3670
  }
3435
3671
  for (const relative6 of listTestFiles(cwd)) {
3436
- const source = read2(join15(cwd, relative6));
3672
+ const source = read2(join16(cwd, relative6));
3437
3673
  if (source === void 0) {
3438
3674
  blockers.push({ file: relative6, reason: "this file could not be read" });
3439
3675
  continue;
3440
3676
  }
3441
3677
  const migrated = migrateTestFile(
3442
- join15(cwd, relative6),
3678
+ join16(cwd, relative6),
3443
3679
  source,
3444
3680
  defaultImported,
3445
3681
  asyncHelpers,
@@ -3466,8 +3702,8 @@ function migrateModuleTests(cwd, setupFiles = []) {
3466
3702
  }
3467
3703
 
3468
3704
  // src/roles/test/nest/generate-config.ts
3469
- import { readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync4 } from "node:fs";
3470
- import { join as join16, relative as relative4 } from "node:path";
3705
+ import { readdirSync as readdirSync5, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
3706
+ import { join as join17, relative as relative4 } from "node:path";
3471
3707
  var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
3472
3708
  var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
3473
3709
  function flavourOf(reading) {
@@ -3513,10 +3749,10 @@ function readModuleTraits(cwd) {
3513
3749
  }
3514
3750
  for (const entry of entries) {
3515
3751
  if (SKIPPED3.has(entry)) continue;
3516
- const path = join16(dir, entry);
3752
+ const path = join17(dir, entry);
3517
3753
  let directory;
3518
3754
  try {
3519
- directory = statSync4(path).isDirectory();
3755
+ directory = statSync5(path).isDirectory();
3520
3756
  } catch {
3521
3757
  continue;
3522
3758
  }
@@ -3527,7 +3763,7 @@ function readModuleTraits(cwd) {
3527
3763
  if (!/\.tsx?$/.test(entry)) continue;
3528
3764
  let source;
3529
3765
  try {
3530
- source = readFileSync13(path, "utf8");
3766
+ source = readFileSync14(path, "utf8");
3531
3767
  } catch {
3532
3768
  continue;
3533
3769
  }
@@ -3660,8 +3896,8 @@ ${overrides.join("\n")}
3660
3896
  }
3661
3897
 
3662
3898
  // src/roles/test/nest/read-jest-config.ts
3663
- import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
3664
- import { join as join17 } from "node:path";
3899
+ import { existsSync as existsSync15, readFileSync as readFileSync15 } from "node:fs";
3900
+ import { join as join18 } from "node:path";
3665
3901
  var CARRIED = [
3666
3902
  "testEnvironment",
3667
3903
  "coverageDirectory",
@@ -3851,7 +4087,7 @@ function extendedConfig(source, cwd) {
3851
4087
  (name) => name.replace(/^\.\//, "")
3852
4088
  );
3853
4089
  const file = candidates.find(
3854
- (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync14(join17(cwd, name))
4090
+ (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync15(join18(cwd, name))
3855
4091
  );
3856
4092
  if (file === void 0) continue;
3857
4093
  if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
@@ -3878,11 +4114,11 @@ function sideEffects(source, extended) {
3878
4114
  return found;
3879
4115
  }
3880
4116
  function readJestConfig(cwd, named) {
3881
- const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync14(join17(cwd, name)));
3882
- if (file === void 0 || !existsSync14(join17(cwd, file))) return void 0;
4117
+ const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync15(join18(cwd, name)));
4118
+ if (file === void 0 || !existsSync15(join18(cwd, file))) return void 0;
3883
4119
  let source;
3884
4120
  try {
3885
- source = readFileSync14(join17(cwd, file), "utf8");
4121
+ source = readFileSync15(join18(cwd, file), "utf8");
3886
4122
  } catch {
3887
4123
  return { file, refusals: [`${file} could not be read`], notes: [] };
3888
4124
  }
@@ -3963,8 +4199,8 @@ function readJestConfig(cwd, named) {
3963
4199
  }
3964
4200
 
3965
4201
  // src/roles/test/nest/run-modes.ts
3966
- import { existsSync as existsSync15 } from "node:fs";
3967
- import { basename as basename2, join as join18 } from "node:path";
4202
+ import { existsSync as existsSync16 } from "node:fs";
4203
+ import { basename as basename2, join as join19 } from "node:path";
3968
4204
  var JEST_EXECUTOR = "@nx/jest:jest";
3969
4205
  var PRIMARY_CONFIG_FILE = "vitest.config.mts";
3970
4206
  var PRIMARY_TARGET = "test";
@@ -3985,7 +4221,7 @@ function runModes(cwd, fallback) {
3985
4221
  const jestConfig = entry.options?.jestConfig;
3986
4222
  if (jestConfig === void 0) continue;
3987
4223
  const name = configName(jestConfig);
3988
- if (!existsSync15(join18(cwd, name))) continue;
4224
+ if (!existsSync16(join19(cwd, name))) continue;
3989
4225
  declared.push({
3990
4226
  target,
3991
4227
  jestConfig: name,
@@ -4010,12 +4246,12 @@ function runModes(cwd, fallback) {
4010
4246
  }
4011
4247
 
4012
4248
  // src/roles/test/written-file-formatter.ts
4013
- import { existsSync as existsSync17 } from "node:fs";
4014
- import { dirname as dirname9, join as join21 } from "node:path";
4249
+ import { existsSync as existsSync18 } from "node:fs";
4250
+ import { dirname as dirname10, join as join22 } from "node:path";
4015
4251
 
4016
4252
  // src/roles/format/read-adoption.ts
4017
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
4018
- import { join as join19 } from "node:path";
4253
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "node:fs";
4254
+ import { join as join20 } from "node:path";
4019
4255
 
4020
4256
  // src/roles/format/config-policy.ts
4021
4257
  var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
@@ -4121,11 +4357,11 @@ function sameValue(a, b) {
4121
4357
  return JSON.stringify(a) === JSON.stringify(b);
4122
4358
  }
4123
4359
  function readFormatAdoption(cwd) {
4124
- const path = join19(cwd, FORMAT_CONFIG_FILE);
4125
- if (!existsSync16(path)) return NOT_ADOPTED(null);
4360
+ const path = join20(cwd, FORMAT_CONFIG_FILE);
4361
+ if (!existsSync17(path)) return NOT_ADOPTED(null);
4126
4362
  let parsed;
4127
4363
  try {
4128
- parsed = parseJsonc(readFileSync15(path, "utf8"), FORMAT_CONFIG_FILE);
4364
+ parsed = parseJsonc(readFileSync16(path, "utf8"), FORMAT_CONFIG_FILE);
4129
4365
  } catch (error) {
4130
4366
  const reason = error instanceof Error ? error.message : String(error);
4131
4367
  return NOT_ADOPTED(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
@@ -4169,9 +4405,9 @@ function readFormatAdoption(cwd) {
4169
4405
  }
4170
4406
 
4171
4407
  // src/roles/format/resolve-oxfmt.ts
4172
- import { readFileSync as readFileSync16 } from "node:fs";
4408
+ import { readFileSync as readFileSync17 } from "node:fs";
4173
4409
  import { createRequire as createRequire4 } from "node:module";
4174
- import { dirname as dirname8, join as join20 } from "node:path";
4410
+ import { dirname as dirname9, join as join21 } from "node:path";
4175
4411
  function resolveOxfmt(cwd) {
4176
4412
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
4177
4413
  }
@@ -4200,8 +4436,8 @@ function configSchema() {
4200
4436
  function readConfigSchema() {
4201
4437
  try {
4202
4438
  const manifest = createRequire4(import.meta.url).resolve("oxfmt/package.json");
4203
- const schemaPath = join20(dirname8(manifest), "configuration_schema.json");
4204
- return JSON.parse(readFileSync16(schemaPath, "utf8"));
4439
+ const schemaPath = join21(dirname9(manifest), "configuration_schema.json");
4440
+ return JSON.parse(readFileSync17(schemaPath, "utf8"));
4205
4441
  } catch {
4206
4442
  return void 0;
4207
4443
  }
@@ -4226,9 +4462,9 @@ function prettierConfigFrom(cwd) {
4226
4462
  let dir = cwd;
4227
4463
  for (; ; ) {
4228
4464
  for (const name of PRETTIER_CONFIGS) {
4229
- if (existsSync17(join21(dir, name))) return join21(dir, name);
4465
+ if (existsSync18(join22(dir, name))) return join22(dir, name);
4230
4466
  }
4231
- const parent = dirname9(dir);
4467
+ const parent = dirname10(dir);
4232
4468
  if (parent === dir) return void 0;
4233
4469
  dir = parent;
4234
4470
  }
@@ -4256,12 +4492,12 @@ function writtenFileFormatter(cwd) {
4256
4492
  var SPEC_TSCONFIG = "tsconfig.spec.json";
4257
4493
  var VITEST_GLOBAL_TYPES = "vitest/globals";
4258
4494
  function specTypesOperation(cwd) {
4259
- const path = join22(cwd, SPEC_TSCONFIG);
4260
- if (!existsSync18(path)) return void 0;
4495
+ const path = join23(cwd, SPEC_TSCONFIG);
4496
+ if (!existsSync19(path)) return void 0;
4261
4497
  let config;
4262
4498
  try {
4263
4499
  config = parseJsonc(
4264
- readFileSync17(path, "utf8"),
4500
+ readFileSync18(path, "utf8"),
4265
4501
  SPEC_TSCONFIG
4266
4502
  );
4267
4503
  } catch {
@@ -4281,9 +4517,9 @@ function specTypesOperation(cwd) {
4281
4517
  }
4282
4518
  function shippedTypesPath(cwd, declaration) {
4283
4519
  try {
4284
- const manifest = createRequire5(join22(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
4285
- const file = join22(dirname10(manifest), declaration);
4286
- if (existsSync18(file)) {
4520
+ const manifest = createRequire5(join23(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
4521
+ const file = join23(dirname11(manifest), declaration);
4522
+ if (existsSync19(file)) {
4287
4523
  const path = relative5(cwd, file);
4288
4524
  return path.startsWith(".") ? path : `./${path}`;
4289
4525
  }
@@ -4302,7 +4538,7 @@ function helpersStillCallingJest(cwd) {
4302
4538
  return;
4303
4539
  }
4304
4540
  for (const entry of entries) {
4305
- const path = join22(dir, entry.name);
4541
+ const path = join23(dir, entry.name);
4306
4542
  if (entry.isDirectory()) {
4307
4543
  if (!skip.has(entry.name)) walk2(path);
4308
4544
  continue;
@@ -4312,7 +4548,7 @@ function helpersStillCallingJest(cwd) {
4312
4548
  if (/^(jest|vitest)\./.test(entry.name)) continue;
4313
4549
  let source;
4314
4550
  try {
4315
- source = readFileSync17(path, "utf8");
4551
+ source = readFileSync18(path, "utf8");
4316
4552
  } catch {
4317
4553
  continue;
4318
4554
  }
@@ -4348,7 +4584,7 @@ function tsconfigFileListOperations(cwd) {
4348
4584
  let config;
4349
4585
  try {
4350
4586
  config = parseJsonc(
4351
- readFileSync17(join22(cwd, name), "utf8"),
4587
+ readFileSync18(join23(cwd, name), "utf8"),
4352
4588
  name
4353
4589
  );
4354
4590
  } catch {
@@ -4816,7 +5052,7 @@ function fromWorkspaceRoot(moduleDir, options, paths) {
4816
5052
  if (root === void 0 || root === moduleDir) {
4817
5053
  return { cwd: moduleDir, options: [...options], paths: [...paths] };
4818
5054
  }
4819
- const absolute = (value) => isAbsolute2(value) ? value : resolve7(moduleDir, value);
5055
+ const absolute = (value) => isAbsolute2(value) ? value : resolve8(moduleDir, value);
4820
5056
  const moved = [];
4821
5057
  let previousWasConfig = false;
4822
5058
  for (const option of options) {
package/dist/index.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  registerAdapters,
7
7
  resolve,
8
8
  setDefaultRunner
9
- } from "./chunk-QYIF3HXC.js";
9
+ } from "./chunk-63NQP75B.js";
10
10
  import {
11
11
  BaseAdapter
12
- } from "./chunk-XM65P73B.js";
12
+ } from "./chunk-UBXL567Z.js";
13
13
  import "./chunk-O7REVMOC.js";
14
14
  import "./chunk-NW6UHNYX.js";
15
15
  export {
@@ -7,7 +7,7 @@ import {
7
7
  readTestAdoption,
8
8
  resolveVitest,
9
9
  suiteOf
10
- } from "./chunk-XM65P73B.js";
10
+ } from "./chunk-UBXL567Z.js";
11
11
  import "./chunk-O7REVMOC.js";
12
12
  import "./chunk-NW6UHNYX.js";
13
13
 
@@ -10,6 +10,7 @@ that decided a design choice say which choice.
10
10
  - [The one line that makes a per-module migration possible](#the-one-line-that-makes-a-per-module-migration-possible)
11
11
  - [What `--init --test` rewrites, and what it refuses](#what---init---test-rewrites-and-what-it-refuses)
12
12
  - [Nest services](#nest-services)
13
+ - [The two DOM matchers, which a react module gets without asking](#the-two-dom-matchers-which-a-react-module-gets-without-asking)
13
14
  - [The check that decides whether you are done](#the-check-that-decides-whether-you-are-done)
14
15
 
15
16
  ## What the ground actually looks like
@@ -292,16 +293,36 @@ member NAMES, which `any` gave up too. Applied only when the SAME file mocks tha
292
293
  ## The check that decides whether you are done
293
294
 
294
295
  Not "the suite is green". **The suite runs the same tests as before**. You do not have to arrange
295
- that comparison; the two commands do it between them:
296
+ that comparison; `--validate` does it, on either side of the migration:
296
297
 
297
298
  ```console
298
- $ sentinel --init --test # runs jest ONCE first, records every test name, then migrates
299
- $ pnpm install # the module's dependencies changed
300
- $ sentinel --run --test # runs vitest, compares against that recording, then spends it
299
+ $ sentinel --validate --test # on the JEST state: records every passing test name
300
+ $ sentinel --init --test # migrates
301
+ $ pnpm install # the module's dependencies changed
302
+ $ sentinel --validate --test # runs vitest, compares against that recording, then spends it
301
303
  ```
302
304
 
303
305
  A codemod touching thousands of files cannot be reviewed by hand. This comparison is the review.
304
306
 
307
+ ⚠️ **The first command comes first, and the tool enforces it.** Start with `--init` and the jest
308
+ config is already gone, so there is nothing left to record and nothing to compare against:
309
+
310
+ ```console
311
+ ✗ recruitment — already migrated, and no reference was recorded before it was, so there is
312
+ nothing to compare against. A proof has to be started BEFORE the migration:
313
+ `sentinel --validate --test` on the jest state, then `--init --test`, then this again.
314
+ ```
315
+
316
+ ### Why the proof is its own verb
317
+
318
+ Because of what SURVIVES a run. `--init` and `--migrate` write state that stays, which is their
319
+ whole purpose and why they are strict about what they touch. `--validate` may instrument the module
320
+ it is proving, provided nothing it writes remains. Those are different contracts, so they are
321
+ different verbs, and the proof can grow without touching a migration that is already proven.
322
+
323
+ It answers every type, too: `sentinel --validate --lint` is a well-formed command, so it says "not
324
+ available yet" rather than calling your sentence wrong.
325
+
305
326
  ### Why it cannot be one command
306
327
 
307
328
  The reference only exists BEFORE the migration, and the thing that produces it, the jest config, is
@@ -321,7 +342,7 @@ shorter.
321
342
  So the gate refuses a run that lost a name or invented one, and says which:
322
343
 
323
344
  ```console
324
- $ sentinel --run --test
345
+ $ sentinel --validate --test
325
346
  1 test(s) no longer exist: useHublerNetworkProfilesQuery builds the URL with employment
326
347
  statuses only. (reference: jest.config.ts, 2026-09-23T21:10:00.000Z) The reference is KEPT
327
348
  until a run passes it, so this does not go green by running again. Fix the suite and re-run,
@@ -356,6 +377,7 @@ have read that improvement as a regression.
356
377
  ```console
357
378
  $ nx run <module>:typecheck # BEFORE, and write that down too
358
379
  $ sentinel --init --test
380
+ $ pnpm install
359
381
  $ nx run <module>:typecheck # after
360
382
  ```
361
383
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.4.0-alpha.31",
3
+ "version": "1.4.0-alpha.33",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",