@hublo/sentinel 1.5.0 → 1.5.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ import {
10
10
  LINT_CONFIG_FILE,
11
11
  LINT_MEASURE_FILE,
12
12
  LINT_SCRIPT_NAME,
13
+ OWNED_WHEN_SET_KEYS,
13
14
  PERMITTED_LOCAL_KEYS,
14
15
  PRETTIER_CONFIG_FILES,
15
16
  PROVENANCE_KEY,
@@ -51,11 +52,12 @@ import {
51
52
  resolveOxfmt,
52
53
  resolveOxlint,
53
54
  rewriteToolchainImports,
55
+ scriptPath,
54
56
  selfCommand,
55
57
  splitToolArgs,
56
58
  walk,
57
59
  writtenFileFormatter
58
- } from "./chunk-43HQ6BVJ.js";
60
+ } from "./chunk-J2USE3KV.js";
59
61
  import {
60
62
  DEFAULT_MAX_DIAGNOSTICS,
61
63
  WORKSPACE_ROOT_MARKER,
@@ -64,7 +66,7 @@ import {
64
66
  ownPackageDir,
65
67
  readOwnPackage,
66
68
  readProjectPackageJson
67
- } from "./chunk-IL5PPLXE.js";
69
+ } from "./chunk-JRZI5IIN.js";
68
70
  import {
69
71
  PRESET_NAMES,
70
72
  REPORT_DIR,
@@ -77,6 +79,9 @@ import {
77
79
  isPlainObject,
78
80
  overridableSettings
79
81
  } from "./chunk-WBDBRGLV.js";
82
+ import {
83
+ globToRegExp
84
+ } from "./chunk-O7REVMOC.js";
80
85
  import {
81
86
  parseJsonc
82
87
  } from "./chunk-QXFCZON7.js";
@@ -153,7 +158,7 @@ function resolve(target, preset, runner) {
153
158
  // src/shared/color.ts
154
159
  import { styleText } from "node:util";
155
160
  function palette(stream) {
156
- const paint = (format, text) => styleText(format, text, { stream });
161
+ const paint = (format2, text) => styleText(format2, text, { stream });
157
162
  return {
158
163
  ok: (text) => paint(["green", "bold"], text),
159
164
  fail: (text) => paint(["red", "bold"], text),
@@ -581,6 +586,19 @@ function composeToolScript(existing, options) {
581
586
  function keepsOtherCommands(script, sentinelCommand) {
582
587
  return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
583
588
  }
589
+ function leadingCommands(script) {
590
+ if (script === void 0) return void 0;
591
+ const parts = script.split(SEGMENT_SEPARATOR);
592
+ const leading = [];
593
+ for (let index = 0; index < parts.length; index += 2) {
594
+ const segment = (parts[index] ?? "").trim();
595
+ if (invokes(segment, "sentinel") || segment.includes("bin/sentinel.js")) break;
596
+ if (index + 1 >= parts.length) return void 0;
597
+ if ((parts[index + 1] ?? "").trim() !== "&&") return void 0;
598
+ leading.push(segment);
599
+ }
600
+ return leading.length > 0 ? leading.join(" && ") : void 0;
601
+ }
584
602
 
585
603
  // src/roles/build/dev-script.ts
586
604
  var CHAIN = " -- ";
@@ -819,8 +837,10 @@ function planReactApp(context2, configFile) {
819
837
  skipped: `${configFile} could not be pointed at sentinel, so nothing was written: ${rewrite.why}. Nothing here is broken; this needs a look before the role applies.`
820
838
  };
821
839
  }
840
+ const rewritten = [];
822
841
  if (rewrite.kind === "rewritten") {
823
842
  operations.push({ kind: "write", path: configFile, contents: rewrite.source });
843
+ rewritten.push(configFile);
824
844
  }
825
845
  for (const name of COMPANION_CONFIG_FILES) {
826
846
  const source = readConfigSource(context2.cwd, name);
@@ -828,6 +848,7 @@ function planReactApp(context2, configFile) {
828
848
  const companion = rewriteBuildImports(source, name);
829
849
  if (companion.kind === "rewritten") {
830
850
  operations.push({ kind: "write", path: name, contents: companion.source });
851
+ rewritten.push(name);
831
852
  notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
832
853
  } else if (companion.kind === "blocked") {
833
854
  notes.push(
@@ -861,10 +882,12 @@ function planReactApp(context2, configFile) {
861
882
  `removed ${owned.map(([, name]) => name).join(", ")} from this module: sentinel owns them now, and two copies of Vite in one build give the plugins a different Vite than the one running them.`
862
883
  );
863
884
  }
864
- const formatted = readFormatAdoption(context2.cwd).adopted;
865
- notes.push(
866
- formatted ? `run \`sentinel --run --format --fix\` on this module: adoption rewrote ${configFile}, and a rewritten import can be longer than your formatter wraps at.` : `this module is formatted by the workspace, not by sentinel, so format ${configFile} with whatever owns it before committing: a rewritten import can be longer than that formatter wraps at, and the check would fail on a file nobody typed.`
867
- );
885
+ if (rewritten.length > 0) {
886
+ const files = rewritten.join(", ");
887
+ notes.push(
888
+ readFormatAdoption(context2.cwd).adopted ? `run \`sentinel --run --format --fix\` on this module: adoption rewrote ${files}, and a rewritten import can be longer than your formatter wraps at.` : `this module is formatted by the workspace, not by sentinel, so format ${files} with whatever owns it before committing: a rewritten import can be longer than that formatter wraps at, and the check would fail on a file nobody typed.`
889
+ );
890
+ }
868
891
  notes.push(
869
892
  `this config is code, and it runs before the build does: any environment variable it reads has to be present wherever you build, including a fresh clone, another CI job or an image. Adoption does not change what it reads, but a failure there arrives early and names this role rather than the variable.`
870
893
  );
@@ -1266,8 +1289,9 @@ function registerBuild() {
1266
1289
 
1267
1290
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1268
1291
  import { spawnSync as spawnSync3 } from "node:child_process";
1269
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
1270
- import { join as join10 } from "node:path";
1292
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
1293
+ import { join as join11 } from "node:path";
1294
+ import { isDeepStrictEqual } from "node:util";
1271
1295
 
1272
1296
  // src/core/config/has-source.ts
1273
1297
  import { readdirSync } from "node:fs";
@@ -1361,6 +1385,160 @@ function scopeFlags(paths, options = []) {
1361
1385
  return [TOLERATE_EMPTY_FLAG];
1362
1386
  }
1363
1387
 
1388
+ // src/roles/format/deliberate-import-order.ts
1389
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
1390
+ import { join as join8 } from "node:path";
1391
+ import { parseSync } from "oxc-parser";
1392
+ import { format } from "oxfmt";
1393
+
1394
+ // src/roles/format/presets/deliberate-import-order.json
1395
+ var deliberate_import_order_default = {
1396
+ rules: [
1397
+ "import/order",
1398
+ "import/first",
1399
+ "import-x/order",
1400
+ "import-x/first",
1401
+ "simple-import-sort/imports",
1402
+ "simple-import-sort/exports"
1403
+ ],
1404
+ modules: [
1405
+ "dd-trace",
1406
+ "dd-trace/**",
1407
+ "reflect-metadata",
1408
+ "source-map-support/register",
1409
+ "@opentelemetry/sdk-**",
1410
+ "@opentelemetry/auto-instrumentations-**",
1411
+ "core-js",
1412
+ "core-js/**",
1413
+ "regenerator-runtime",
1414
+ "regenerator-runtime/**"
1415
+ ]
1416
+ };
1417
+
1418
+ // src/roles/format/deliberate-import-order.ts
1419
+ var PIN_COMMENT = "// oxfmt-ignore";
1420
+ var CODE_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx"];
1421
+ var ORDER_RULES = deliberate_import_order_default.rules;
1422
+ var LOAD_ORDER_MODULES = deliberate_import_order_default.modules.map((glob) => globToRegExp(glob));
1423
+ var MODULE_NAMES = [
1424
+ ...new Set(deliberate_import_order_default.modules.map((glob) => glob.split("*")[0] ?? glob))
1425
+ ];
1426
+ function disabledRules(comment, directive) {
1427
+ const match = new RegExp(String.raw`^\s*${directive}(?:\s+(.*?))?\s*(?:--.*)?$`).exec(comment);
1428
+ if (match === null) return void 0;
1429
+ return (match[1] ?? "").split(/[\s,]+/).filter(Boolean);
1430
+ }
1431
+ function namesOrderRule(rules) {
1432
+ return rules !== void 0 && rules.some((rule) => ORDER_RULES.includes(rule));
1433
+ }
1434
+ function lineOf(starts, offset) {
1435
+ let low = 0;
1436
+ let high = starts.length - 1;
1437
+ while (low < high) {
1438
+ const middle = Math.ceil((low + high) / 2);
1439
+ if ((starts[middle] ?? 0) <= offset) low = middle;
1440
+ else high = middle - 1;
1441
+ }
1442
+ return low;
1443
+ }
1444
+ function readImports(fileName, text) {
1445
+ const { program, comments, errors } = parseSync(fileName, text, { sourceType: "module" });
1446
+ if (errors.length > 0) return [];
1447
+ const starts = [0];
1448
+ for (let index = 0; index < text.length; index++) if (text[index] === "\n") starts.push(index + 1);
1449
+ const nextLine = /* @__PURE__ */ new Set();
1450
+ const sameLine = /* @__PURE__ */ new Set();
1451
+ const regions = [];
1452
+ let regionStart;
1453
+ for (const comment of comments) {
1454
+ const line = lineOf(starts, comment.start);
1455
+ if (comment.type === "Line") {
1456
+ if (namesOrderRule(disabledRules(comment.value, "eslint-disable-next-line"))) {
1457
+ nextLine.add(lineOf(starts, comment.end) + 1);
1458
+ }
1459
+ if (namesOrderRule(disabledRules(comment.value, "eslint-disable-line"))) sameLine.add(line);
1460
+ continue;
1461
+ }
1462
+ if (regionStart === void 0 && namesOrderRule(disabledRules(comment.value, "eslint-disable"))) {
1463
+ regionStart = comment.end;
1464
+ continue;
1465
+ }
1466
+ const enabled = disabledRules(comment.value, "eslint-enable");
1467
+ if (regionStart !== void 0 && enabled !== void 0) {
1468
+ if (enabled.length === 0 || namesOrderRule(enabled)) {
1469
+ regions.push([regionStart, comment.start]);
1470
+ regionStart = void 0;
1471
+ }
1472
+ }
1473
+ }
1474
+ if (regionStart !== void 0) regions.push([regionStart, text.length]);
1475
+ const imports = [];
1476
+ for (const node of program.body) {
1477
+ if (node.type !== "ImportDeclaration") continue;
1478
+ const line = lineOf(starts, node.start);
1479
+ const source = node.source.value;
1480
+ const typeOnly = node.importKind === "type";
1481
+ const signalled = !typeOnly && (LOAD_ORDER_MODULES.some((pattern) => pattern.test(source)) || nextLine.has(line) || sameLine.has(line) || regions.some(([from, to]) => node.start >= from && node.start < to));
1482
+ imports.push({ line, source, signalled });
1483
+ }
1484
+ return imports;
1485
+ }
1486
+ function wouldMove(before, after) {
1487
+ const order = (list) => list.map((entry) => entry.source);
1488
+ const was = order(before);
1489
+ const now = order(after);
1490
+ return before.some((entry, at) => {
1491
+ if (!entry.signalled) return false;
1492
+ const earlier = new Set(was.slice(0, at));
1493
+ const index = now.indexOf(entry.source);
1494
+ const nowEarlier = new Set(now.slice(0, index));
1495
+ return earlier.size !== nowEarlier.size || [...earlier].some((name) => !nowEarlier.has(name));
1496
+ });
1497
+ }
1498
+ function pin(text, imports) {
1499
+ const lines = text.split("\n");
1500
+ const insertAt = [];
1501
+ for (const { line, signalled } of imports) {
1502
+ if (!signalled) continue;
1503
+ let top = line;
1504
+ while (top > 0 && /^\s*\/\//.test(lines[top - 1] ?? "")) top -= 1;
1505
+ if (lines.slice(top, line).some((above) => above.trim() === PIN_COMMENT)) continue;
1506
+ insertAt.push(top);
1507
+ }
1508
+ for (const at of [...new Set(insertAt)].sort((a, b) => b - a)) {
1509
+ const indent = /^\s*/.exec(lines[at] ?? "")?.[0] ?? "";
1510
+ lines.splice(at, 0, `${indent}${PIN_COMMENT}`);
1511
+ }
1512
+ return lines.join("\n");
1513
+ }
1514
+ function formatOptions(config) {
1515
+ const {
1516
+ $sentinel: _provenance,
1517
+ ignorePatterns: _ignored,
1518
+ overrides: _overrides,
1519
+ ...options
1520
+ } = config;
1521
+ return options;
1522
+ }
1523
+ async function pinDeliberateImportOrder(cwd, options) {
1524
+ if (!options.sortImports) return [];
1525
+ const pinned = [];
1526
+ for (const file of listSourceFiles(cwd, CODE_EXTENSIONS).sort()) {
1527
+ const path2 = join8(cwd, file);
1528
+ const text = readFileSync6(path2, "utf8");
1529
+ if (!text.includes("eslint-disable") && !MODULE_NAMES.some((name) => text.includes(name)))
1530
+ continue;
1531
+ const before = readImports(file, text);
1532
+ if (!before.some((entry) => entry.signalled)) continue;
1533
+ const sorted = await format(file, text, options);
1534
+ if (sorted.errors.length > 0) continue;
1535
+ if (!wouldMove(before, readImports(file, sorted.code))) continue;
1536
+ writeFileSync2(path2, pin(text, before));
1537
+ pinned.push(file);
1538
+ }
1539
+ return pinned;
1540
+ }
1541
+
1364
1542
  // src/roles/format/format-script.ts
1365
1543
  var SENTINEL_FORMAT_COMMAND = "sentinel --run --format";
1366
1544
  function isPrettierSegment(segment) {
@@ -1389,8 +1567,8 @@ function writesWhenRewritten(name, command) {
1389
1567
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1390
1568
 
1391
1569
  // src/roles/format/inherited-ignores.ts
1392
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
1393
- import { join as join8 } from "node:path";
1570
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
1571
+ import { join as join9 } from "node:path";
1394
1572
  var ROOT_IGNORE_FILE = ".prettierignore";
1395
1573
  function isPattern(line) {
1396
1574
  const trimmed = line.trim();
@@ -1406,11 +1584,11 @@ function toModulePattern(pattern) {
1406
1584
  }
1407
1585
  function inheritedIgnorePatterns(workspaceRoot) {
1408
1586
  if (!workspaceRoot) return [];
1409
- const path2 = join8(workspaceRoot, ROOT_IGNORE_FILE);
1587
+ const path2 = join9(workspaceRoot, ROOT_IGNORE_FILE);
1410
1588
  if (!existsSync6(path2)) return [];
1411
1589
  let contents;
1412
1590
  try {
1413
- contents = readFileSync6(path2, "utf8");
1591
+ contents = readFileSync7(path2, "utf8");
1414
1592
  } catch {
1415
1593
  return [];
1416
1594
  }
@@ -1455,8 +1633,8 @@ function needsSvelteCompiler(output) {
1455
1633
  }
1456
1634
 
1457
1635
  // src/roles/format/prettier-config.ts
1458
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
1459
- import { join as join9 } from "node:path";
1636
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
1637
+ import { join as join10 } from "node:path";
1460
1638
  var UNSUPPORTED = /* @__PURE__ */ new Set(["plugins", "parser", "filepath", "rangeStart", "rangeEnd"]);
1461
1639
  function toOxfmtOverrides(value) {
1462
1640
  if (!Array.isArray(value)) return { overrides: [], unresolved: [] };
@@ -1481,14 +1659,14 @@ function toOxfmtOverrides(value) {
1481
1659
  return { overrides, unresolved };
1482
1660
  }
1483
1661
  function readPrettierSettings(cwd) {
1484
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync7(join9(cwd, name)));
1662
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync7(join10(cwd, name)));
1485
1663
  if (!file) return { options: {}, unresolved: [] };
1486
1664
  if (/\.(js|cjs|mjs)$/.test(file)) {
1487
1665
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1488
1666
  }
1489
1667
  let parsed;
1490
1668
  try {
1491
- parsed = parseJsonc(readFileSync7(join9(cwd, file), "utf8"), file);
1669
+ parsed = parseJsonc(readFileSync8(join10(cwd, file), "utf8"), file);
1492
1670
  } catch {
1493
1671
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1494
1672
  }
@@ -1570,6 +1748,11 @@ var OxfmtAdapter = class extends BaseAdapter {
1570
1748
  const existing = readFormatAdoption(context2.cwd);
1571
1749
  const prettier = readPrettierSettings(context2.cwd);
1572
1750
  const local = existing.adopted ? { ...existing.local } : { ...prettier.options };
1751
+ const recorded = existing.preset === null ? void 0 : formatPresetFor(existing.preset);
1752
+ const ownedWhenSet = existing.adopted ? OWNED_WHEN_SET_KEYS.filter(
1753
+ (key) => !(key in local) && key in existing.effective && !isDeepStrictEqual(existing.effective[key], preset[key]) && !isDeepStrictEqual(existing.effective[key], recorded?.[key])
1754
+ ) : [];
1755
+ for (const key of ownedWhenSet) local[key] = existing.effective[key];
1573
1756
  for (const [key, value] of Object.entries(local)) {
1574
1757
  if (ALWAYS_LOCAL_KEYS.includes(key)) continue;
1575
1758
  if (key in preset && JSON.stringify(preset[key]) === JSON.stringify(value)) delete local[key];
@@ -1581,17 +1764,19 @@ var OxfmtAdapter = class extends BaseAdapter {
1581
1764
  .../* @__PURE__ */ new Set([...declaredIgnores, ...inherited, ...FORMAT_DEFAULT_IGNORES])
1582
1765
  ].sort();
1583
1766
  if (ignorePatterns.length > 0) local.ignorePatterns = ignorePatterns;
1584
- const unchanged = existing.adopted && existing.conformant;
1585
- const version = unchanged && existing.presetVersion ? existing.presetVersion : own.version;
1586
- const config = {
1587
- [PROVENANCE_KEY]: { preset: presetName, version, local: localKeys },
1767
+ const configStamped = (version2) => ({
1768
+ [PROVENANCE_KEY]: { preset: presetName, version: version2, local: localKeys },
1588
1769
  ...preset,
1589
1770
  ...Object.fromEntries(
1590
1771
  [...localKeys, ...ALWAYS_LOCAL_KEYS].flatMap(
1591
1772
  (key) => key in local ? [[key, local[key]]] : []
1592
1773
  )
1593
1774
  )
1594
- };
1775
+ });
1776
+ const previous = existing.adopted ? existing.presetVersion : null;
1777
+ const unchanged = previous !== null && isDeepStrictEqual(configStamped(previous), existing.effective);
1778
+ const version = unchanged ? previous : own.version;
1779
+ const config = configStamped(version);
1595
1780
  const operations = [
1596
1781
  // Written pre-formatted (2-space, trailing newline): oxfmt formats JSON too, and until
1597
1782
  // the format pass runs this file is the one config nothing has formatted yet.
@@ -1599,7 +1784,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1599
1784
  manifestOperation(context2.cwd, this.formatScripts(context2.cwd))
1600
1785
  ];
1601
1786
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1602
- (name) => existsSync8(join10(context2.cwd, name))
1787
+ (name) => existsSync8(join11(context2.cwd, name))
1603
1788
  );
1604
1789
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1605
1790
  const removableDeps = this.modulePrettierDependencies(context2.cwd);
@@ -1608,8 +1793,13 @@ var OxfmtAdapter = class extends BaseAdapter {
1608
1793
  }
1609
1794
  operations.push(...nxTargetOperations({ cwd: context2.cwd, targets: formatTargets() }));
1610
1795
  const notes = [
1611
- `materialized the ${presetName} format preset into ${FORMAT_CONFIG_FILE} (${own.name}@${own.version}). oxfmt has no \`extends\`, so the values live here; \`sentinel --status --format\` reports modules left behind by a preset change`
1796
+ `materialized the ${presetName} format preset into ${FORMAT_CONFIG_FILE} (${own.name}@${version}${unchanged && version !== own.version ? ", unchanged since" : ""}). oxfmt has no \`extends\`, so the values live here; \`sentinel --status --format\` reports modules left behind by a preset change`
1612
1797
  ];
1798
+ if (ownedWhenSet.length > 0) {
1799
+ notes.push(
1800
+ `kept this module's own ${ownedWhenSet.join(", ")} and declared it in \`${PROVENANCE_KEY}.local\`: the module set it, so a re-init does not take it back`
1801
+ );
1802
+ }
1613
1803
  if (localKeys.length > 0) {
1614
1804
  notes.push(
1615
1805
  `kept this module's own formatting (${localKeys.join(", ")})${existing.adopted ? "" : ` from ${prettier.file}`}: the preset is the repo's, these are yours`
@@ -1655,9 +1845,41 @@ var OxfmtAdapter = class extends BaseAdapter {
1655
1845
  * walks targets in order), so what the linter rewrote gets formatted rather than the
1656
1846
  * other way round.
1657
1847
  */
1848
+ /**
1849
+ * The same `oxfmt --write .`, in the copy `--init --dry-run` hands it, and silent. See
1850
+ * `Adapter.rehearseAfterInit`. It runs after the lint rehearsal for the reason `afterInit` runs
1851
+ * after the lint fix, so what the linter rewrote is what gets formatted.
1852
+ *
1853
+ * The copy sits where the module sits in a stand-in workspace, and that matters to this tool in
1854
+ * particular: oxfmt reads the nearest `.editorconfig` upwards (verified: an `indent_style = tab`
1855
+ * two levels up reindented a file), and the root of this repo has one.
1856
+ */
1857
+ async rehearseAfterInit(ctx, origin) {
1858
+ const oxfmt = resolveOxfmt(origin);
1859
+ if (!oxfmt) return "oxfmt is not installed in this module yet";
1860
+ await this.keepDeliberateImportOrder(ctx.cwd);
1861
+ const pass = spawnSync3(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1862
+ if (pass.error) return pass.error.message;
1863
+ return pass.status === 0 ? void 0 : oxfmtSaid(`${pass.stdout ?? ""}${pass.stderr ?? ""}`);
1864
+ }
1865
+ /**
1866
+ * Pin, before the formatter sorts, the imports whose order is deliberate. See
1867
+ * `deliberate-import-order.ts`. Returns the line to print, or undefined when nothing was pinned.
1868
+ */
1869
+ async keepDeliberateImportOrder(cwd) {
1870
+ const pinned = await pinDeliberateImportOrder(
1871
+ cwd,
1872
+ formatOptions(readFormatAdoption(cwd).effective)
1873
+ );
1874
+ if (pinned.length === 0) return void 0;
1875
+ return `kept the import order of ${pinned.length} file(s) where it is deliberate (an eslint-disable naming an import-order rule, or an import that has to load first, such as dd-trace): ${pinned.join(", ")}. Each such import now carries \`${PIN_COMMENT}\`, so the import sort leaves it where it is`;
1876
+ }
1658
1877
  async afterInit(ctx) {
1659
1878
  const oxfmt = resolveOxfmt(ctx.cwd);
1660
1879
  if (!oxfmt) return;
1880
+ const kept = await this.keepDeliberateImportOrder(ctx.cwd);
1881
+ if (kept !== void 0) process.stderr.write(` ${palette(process.stderr).warn(kept)}
1882
+ `);
1661
1883
  const pass = spawnSync3(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1662
1884
  process.stderr.write(` ${palette(process.stderr).dim(FORMATTER_DIFFERENCES)}
1663
1885
  `);
@@ -1839,7 +2061,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1839
2061
  modulePrettierDependencies(cwd) {
1840
2062
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
1841
2063
  try {
1842
- const manifest = JSON.parse(readFileSync8(join10(cwd, "package.json"), "utf8"));
2064
+ const manifest = JSON.parse(readFileSync9(join11(cwd, "package.json"), "utf8"));
1843
2065
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
1844
2066
  } catch {
1845
2067
  return [];
@@ -1921,8 +2143,8 @@ function registerFormat() {
1921
2143
 
1922
2144
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1923
2145
  import { spawnSync as spawnSync4 } from "node:child_process";
1924
- import { existsSync as existsSync12, readFileSync as readFileSync13, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
1925
- import { join as join16 } from "node:path";
2146
+ import { existsSync as existsSync12, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
2147
+ import { join as join17 } from "node:path";
1926
2148
 
1927
2149
  // src/core/config/deferred-rules.ts
1928
2150
  function deferredRuleNames(rules) {
@@ -1931,7 +2153,7 @@ function deferredRuleNames(rules) {
1931
2153
 
1932
2154
  // src/core/fix-report.ts
1933
2155
  import { statSync as statSync2 } from "node:fs";
1934
- import { join as join11 } from "node:path";
2156
+ import { join as join12 } from "node:path";
1935
2157
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
1936
2158
  function readFailure(output) {
1937
2159
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -1951,7 +2173,7 @@ function fingerprint(path2) {
1951
2173
  function snapshotFiles(cwd, extensions) {
1952
2174
  const snapshot = /* @__PURE__ */ new Map();
1953
2175
  for (const file of listSourceFiles(cwd, extensions)) {
1954
- const mark = fingerprint(join11(cwd, file));
2176
+ const mark = fingerprint(join12(cwd, file));
1955
2177
  if (mark !== void 0) snapshot.set(file, mark);
1956
2178
  }
1957
2179
  return snapshot;
@@ -1959,7 +2181,7 @@ function snapshotFiles(cwd, extensions) {
1959
2181
  function changedSince(cwd, before, extensions) {
1960
2182
  const changed = [];
1961
2183
  for (const file of listSourceFiles(cwd, extensions)) {
1962
- const now = fingerprint(join11(cwd, file));
2184
+ const now = fingerprint(join12(cwd, file));
1963
2185
  if (now === void 0) continue;
1964
2186
  if (before.get(file) !== now) changed.push(file);
1965
2187
  }
@@ -1967,6 +2189,9 @@ function changedSince(cwd, before, extensions) {
1967
2189
  }
1968
2190
  var NAMED_IN_REPORT = 5;
1969
2191
  var NOT_INSTALLED_YET = /invalid config file[\s\S]*node_modules/i;
2192
+ function presetNotInstalledYet(failure) {
2193
+ return NOT_INSTALLED_YET.test(failure);
2194
+ }
1970
2195
  function describeFixOutcome(outcome, toolLabel) {
1971
2196
  if (outcome.failure !== void 0) {
1972
2197
  if (NOT_INSTALLED_YET.test(outcome.failure)) {
@@ -4402,14 +4627,14 @@ function downgradedRulesFor(preset) {
4402
4627
  }
4403
4628
 
4404
4629
  // src/roles/lint/extra-layers.ts
4405
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
4406
- import { isAbsolute, join as join12, resolve as resolve2 } from "node:path";
4630
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
4631
+ import { isAbsolute, join as join13, resolve as resolve2 } from "node:path";
4407
4632
  function ruleCount(cwd, specifier) {
4408
4633
  const path2 = isAbsolute(specifier) ? specifier : resolve2(cwd, specifier);
4409
4634
  if (!existsSync9(path2)) return void 0;
4410
4635
  try {
4411
4636
  const parsed = parseJsonc(
4412
- readFileSync9(path2, "utf8"),
4637
+ readFileSync10(path2, "utf8"),
4413
4638
  specifier
4414
4639
  );
4415
4640
  return Object.keys(parsed.rules ?? {}).length;
@@ -4429,7 +4654,7 @@ function extraLayers(cwd, extendsList) {
4429
4654
  function committedExtendsList(cwd, configFile) {
4430
4655
  try {
4431
4656
  const parsed = parseJsonc(
4432
- readFileSync9(join12(cwd, configFile), "utf8"),
4657
+ readFileSync10(join13(cwd, configFile), "utf8"),
4433
4658
  configFile
4434
4659
  );
4435
4660
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4488,13 +4713,13 @@ function errorCountsByConfigRule(stdout) {
4488
4713
  }
4489
4714
 
4490
4715
  // src/roles/lint/adapters/oxlint/plan.ts
4491
- import { existsSync as existsSync11, readFileSync as readFileSync12 } from "node:fs";
4492
- import { join as join15 } from "node:path";
4716
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
4717
+ import { join as join16 } from "node:path";
4493
4718
 
4494
4719
  // src/roles/lint/eslint-ignores.ts
4495
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
4496
- import { join as join13 } from "node:path";
4497
- import { parseSync } from "oxc-parser";
4720
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
4721
+ import { join as join14 } from "node:path";
4722
+ import { parseSync as parseSync2 } from "oxc-parser";
4498
4723
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4499
4724
  function toOxlintPattern(pattern) {
4500
4725
  return pattern.replace(/^\.\//, "");
@@ -4505,17 +4730,17 @@ function readRootEslintIgnores(root) {
4505
4730
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4506
4731
  }
4507
4732
  function readEslintIgnores(cwd) {
4508
- const config = ESLINT_CONFIG_FILES.map((name) => join13(cwd, name)).find((path2) => existsSync10(path2));
4733
+ const config = ESLINT_CONFIG_FILES.map((name) => join14(cwd, name)).find((path2) => existsSync10(path2));
4509
4734
  if (!config) return { patterns: [], unresolved: [] };
4510
4735
  let source;
4511
4736
  try {
4512
- source = readFileSync10(config, "utf8");
4737
+ source = readFileSync11(config, "utf8");
4513
4738
  } catch {
4514
4739
  return { patterns: [], unresolved: [] };
4515
4740
  }
4516
4741
  const patterns = /* @__PURE__ */ new Set();
4517
4742
  const unresolved = new Set([...source.matchAll(OPAQUE_SOURCE)].map((match) => match[0]));
4518
- const { program, errors } = parseSync(config, source, { sourceType: "module" });
4743
+ const { program, errors } = parseSync2(config, source, { sourceType: "module" });
4519
4744
  if (errors.length > 0) {
4520
4745
  return { patterns: [], unresolved: [`${config} could not be parsed`] };
4521
4746
  }
@@ -4579,6 +4804,98 @@ function keepsOtherCommands3(script) {
4579
4804
  return keepsOtherCommands(script, SENTINEL_LINT_COMMAND);
4580
4805
  }
4581
4806
 
4807
+ // src/roles/lint/module-own-config.ts
4808
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4809
+ function pluginKey(entry) {
4810
+ if (typeof entry === "string") return entry;
4811
+ if (isRecord(entry) && typeof entry.name === "string") return entry.name;
4812
+ if (isRecord(entry) && typeof entry.specifier === "string") return entry.specifier;
4813
+ return void 0;
4814
+ }
4815
+ function governedRuleNames(preset) {
4816
+ return /* @__PURE__ */ new Set([
4817
+ ...Object.keys(preset.rules),
4818
+ ...(preset.overrides ?? []).flatMap((override) => Object.keys(override.rules))
4819
+ ]);
4820
+ }
4821
+ function moduleOwnBlocks(committed, preset) {
4822
+ const blocks = {};
4823
+ const kept = [];
4824
+ const dropped = [];
4825
+ if (Array.isArray(committed.plugins)) {
4826
+ const own = committed.plugins.filter(
4827
+ (name) => typeof name === "string" && !preset.plugins.includes(name)
4828
+ );
4829
+ if (own.length > 0) {
4830
+ blocks.plugins = own;
4831
+ kept.push(
4832
+ `kept this module's own plugins (${own.join(", ")}): the preset does not enable them`
4833
+ );
4834
+ }
4835
+ }
4836
+ if (Array.isArray(committed.jsPlugins)) {
4837
+ const presetKeys = new Set(preset.jsPlugins.map((entry) => pluginKey(entry)));
4838
+ const own = [];
4839
+ for (const entry of committed.jsPlugins) {
4840
+ const key = pluginKey(entry);
4841
+ if (key !== void 0 && presetKeys.has(key)) dropped.push(`jsPlugins ${key}`);
4842
+ else own.push(entry);
4843
+ }
4844
+ if (own.length > 0) {
4845
+ blocks.jsPlugins = own;
4846
+ kept.push(
4847
+ `kept ${own.length} jsPlugin(s) this module added (${own.map((entry) => pluginKey(entry) ?? "?").join(", ")})`
4848
+ );
4849
+ }
4850
+ }
4851
+ if (isRecord(committed.settings)) {
4852
+ const presetSettings = preset.settings ?? {};
4853
+ const own = {};
4854
+ for (const [key, value] of Object.entries(committed.settings)) {
4855
+ if (key in presetSettings) dropped.push(`settings.${key}`);
4856
+ else own[key] = value;
4857
+ }
4858
+ if (Object.keys(own).length > 0) {
4859
+ blocks.settings = own;
4860
+ kept.push(`kept this module's own settings (${Object.keys(own).join(", ")})`);
4861
+ }
4862
+ }
4863
+ if (Array.isArray(committed.overrides)) {
4864
+ const governed = governedRuleNames(preset);
4865
+ const byFiles = /* @__PURE__ */ new Map();
4866
+ for (const block of committed.overrides) {
4867
+ if (!isRecord(block) || !Array.isArray(block.files)) continue;
4868
+ const files = JSON.stringify([...block.files].map(String).sort());
4869
+ const rules = {};
4870
+ for (const [rule, value] of Object.entries(isRecord(block.rules) ? block.rules : {})) {
4871
+ if (governed.has(rule)) dropped.push(`overrides[${block.files.join(", ")}] ${rule}`);
4872
+ else rules[rule] = value;
4873
+ }
4874
+ const { rules: _rules, ...rest } = block;
4875
+ const merged = byFiles.get(files);
4876
+ if (merged === void 0) {
4877
+ byFiles.set(files, { ...rest, ...Object.keys(rules).length > 0 ? { rules } : {} });
4878
+ continue;
4879
+ }
4880
+ const { files: _sameFiles, ...others } = rest;
4881
+ const mergedRules = { ...isRecord(merged.rules) ? merged.rules : {}, ...rules };
4882
+ Object.assign(
4883
+ merged,
4884
+ others,
4885
+ Object.keys(mergedRules).length > 0 ? { rules: mergedRules } : {}
4886
+ );
4887
+ }
4888
+ const own = [...byFiles.values()].filter((block) => Object.keys(block).length > 1);
4889
+ if (own.length > 0) {
4890
+ blocks.overrides = own;
4891
+ kept.push(
4892
+ `kept ${own.length} override block(s) this module set (${own.map((block) => block.files.join(", ")).join("; ")})`
4893
+ );
4894
+ }
4895
+ }
4896
+ return { blocks, kept, dropped };
4897
+ }
4898
+
4582
4899
  // src/roles/lint/presets.ts
4583
4900
  var hasRuleData = (name) => GENERATED_LINT_PRESETS.includes(name);
4584
4901
  function lintPresetFor(preset) {
@@ -4600,8 +4917,8 @@ function lintPresetFor(preset) {
4600
4917
  }
4601
4918
 
4602
4919
  // src/roles/lint/rename-suppressions.ts
4603
- import { readdirSync as readdirSync2, readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
4604
- import { join as join14, relative as relative3 } from "node:path";
4920
+ import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
4921
+ import { join as join15, relative as relative3 } from "node:path";
4605
4922
  var SOURCE_EXTENSIONS = [
4606
4923
  ".ts",
4607
4924
  ".tsx",
@@ -4648,7 +4965,7 @@ function* sourceFiles(dir) {
4648
4965
  return;
4649
4966
  }
4650
4967
  for (const entry of entries) {
4651
- const full = join14(dir, entry);
4968
+ const full = join15(dir, entry);
4652
4969
  let isDirectory;
4653
4970
  try {
4654
4971
  isDirectory = statSync3(full).isDirectory();
@@ -4668,7 +4985,7 @@ function findSuppressionRenames(cwd, renames) {
4668
4985
  for (const file of sourceFiles(cwd)) {
4669
4986
  let content;
4670
4987
  try {
4671
- content = readFileSync11(file, "utf8");
4988
+ content = readFileSync12(file, "utf8");
4672
4989
  } catch {
4673
4990
  continue;
4674
4991
  }
@@ -4788,8 +5105,18 @@ function plan2(context2) {
4788
5105
  ...rootIgnores.patterns
4789
5106
  ].filter((pattern) => !defaults2.has(bare(pattern)));
4790
5107
  const carried = [.../* @__PURE__ */ new Set([...DEFAULT_IGNORE_PATTERNS, ...moduleOwn])];
5108
+ const committed = committedConfig(context2.cwd);
5109
+ const { kept: teamRules, governed: droppedRules } = moduleOwnRules(
5110
+ isRecord2(committed.rules) ? committed.rules : {},
5111
+ lintPresetFor(variant)
5112
+ );
5113
+ const teamBlocks = moduleOwnBlocks(committed, lintPresetFor(variant));
5114
+ const { overrides: teamOverrides, ...teamTopLevel } = teamBlocks.blocks;
4791
5115
  const stub = {
4792
5116
  extends: extendsWithPreset(committedExtends(context2.cwd), variant),
5117
+ ...teamTopLevel,
5118
+ ...Object.keys(teamRules).length > 0 ? { rules: teamRules } : {},
5119
+ ...teamOverrides !== void 0 ? { overrides: teamOverrides } : {},
4793
5120
  ...carried.length > 0 ? { ignorePatterns: carried } : {}
4794
5121
  };
4795
5122
  const operations = [
@@ -4804,7 +5131,7 @@ function plan2(context2) {
4804
5131
  keys: removableDeps
4805
5132
  });
4806
5133
  }
4807
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync11(join15(context2.cwd, name)));
5134
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync11(join16(context2.cwd, name)));
4808
5135
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4809
5136
  operations.push(
4810
5137
  ...nxTargetOperations({
@@ -4838,6 +5165,22 @@ function plan2(context2) {
4838
5165
  if (eslintConfigs.length > 0) {
4839
5166
  notes.push(`removed ${eslintConfigs.join(", ")} and pointed the nx lint target at oxlint`);
4840
5167
  }
5168
+ if (Object.keys(teamRules).length > 0) {
5169
+ notes.push(
5170
+ `kept ${Object.keys(teamRules).length} rule(s) this module set itself (${Object.keys(teamRules).join(", ")}): the preset does not configure them`
5171
+ );
5172
+ }
5173
+ if (droppedRules.length > 0) {
5174
+ notes.push(
5175
+ `dropped ${droppedRules.length} rule(s) this module set over the preset (${droppedRules.join(", ")}): the preset governs them`
5176
+ );
5177
+ }
5178
+ notes.push(...teamBlocks.kept);
5179
+ if (teamBlocks.dropped.length > 0) {
5180
+ notes.push(
5181
+ `dropped ${teamBlocks.dropped.length} entr(ies) this module set over the preset (${teamBlocks.dropped.join(", ")}): the preset governs them`
5182
+ );
5183
+ }
4841
5184
  if (rootIgnores.patterns.length > 0) {
4842
5185
  notes.push(
4843
5186
  `carried ${rootIgnores.patterns.length} ignore pattern(s) from the workspace eslint config (${rootIgnores.patterns.join(", ")}): this module is linted by that config today, so without them adoption would start linting what the repo has always excluded`
@@ -4867,7 +5210,7 @@ function plan2(context2) {
4867
5210
  if (keepsOtherCommands3(script)) {
4868
5211
  const stillPrettier = /(^|[\s/])prettier(\s|$)/.test(script);
4869
5212
  notes.push(
4870
- `kept the rest of the lint script (\`${script}\`): only the eslint part is sentinel's` + (stillPrettier ? ". Run `sentinel --init --format` to move the formatting part too" : "")
5213
+ `kept the rest of the lint script (\`${script}\`): only the eslint part is sentinel's` + (stillPrettier ? "; the format role, adopted with it, moves the prettier part" : "")
4871
5214
  );
4872
5215
  }
4873
5216
  notes.push(
@@ -4902,7 +5245,7 @@ function lintScripts(cwd) {
4902
5245
  function committedExtends(cwd) {
4903
5246
  try {
4904
5247
  const parsed = parseJsonc(
4905
- readFileSync12(join15(cwd, LINT_CONFIG_FILE), "utf8"),
5248
+ readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
4906
5249
  LINT_CONFIG_FILE
4907
5250
  );
4908
5251
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4910,10 +5253,32 @@ function committedExtends(cwd) {
4910
5253
  return [];
4911
5254
  }
4912
5255
  }
5256
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5257
+ function committedConfig(cwd) {
5258
+ try {
5259
+ const parsed = parseJsonc(
5260
+ readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
5261
+ LINT_CONFIG_FILE
5262
+ );
5263
+ return isRecord2(parsed) ? parsed : {};
5264
+ } catch {
5265
+ return {};
5266
+ }
5267
+ }
5268
+ function moduleOwnRules(committed, preset) {
5269
+ const governedNames = governedRuleNames(preset);
5270
+ const kept = {};
5271
+ const governed = [];
5272
+ for (const [rule, value] of Object.entries(committed)) {
5273
+ if (governedNames.has(rule)) governed.push(rule);
5274
+ else kept[rule] = value;
5275
+ }
5276
+ return { kept, governed };
5277
+ }
4913
5278
  function committedIgnorePatterns(cwd) {
4914
5279
  try {
4915
5280
  const parsed = parseJsonc(
4916
- readFileSync12(join15(cwd, LINT_CONFIG_FILE), "utf8"),
5281
+ readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
4917
5282
  LINT_CONFIG_FILE
4918
5283
  );
4919
5284
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4925,7 +5290,7 @@ function moduleEslintDependencies(cwd) {
4925
5290
  const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
4926
5291
  let manifest;
4927
5292
  try {
4928
- manifest = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
5293
+ manifest = JSON.parse(readFileSync13(join16(cwd, "package.json"), "utf8"));
4929
5294
  } catch {
4930
5295
  return [];
4931
5296
  }
@@ -5039,7 +5404,54 @@ ${result.stderr ?? ""}`);
5039
5404
  const env = { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) };
5040
5405
  process.stderr.write(` ${describeFixOutcome(this.fixPass(ctx, oxlint, env), "lint")}
5041
5406
  `);
5042
- this.writeModuleBaseline(ctx, oxlint, env);
5407
+ if (this.runLeadingCommands(ctx, env)) this.writeModuleBaseline(ctx, oxlint, env);
5408
+ }
5409
+ /**
5410
+ * Run what the module's `lint` script runs before sentinel, so the baseline measures the module
5411
+ * `pnpm run lint` lints. See `leadingCommands`: on hr-management that is `svelte-kit sync`, and
5412
+ * without it the measurement met a tsconfig extending a file that did not exist yet and held
5413
+ * `typescript/tsconfig-error`, a hold a fresh clone would commit.
5414
+ *
5415
+ * Shelled the way a package manager runs a script, with every `node_modules/.bin` up the tree
5416
+ * on PATH. False when the command failed: a baseline measured without it is the wrong module,
5417
+ * and recording that is worse than recording nothing, so the existing baseline is left alone
5418
+ * and the developer is told what to run.
5419
+ */
5420
+ runLeadingCommands(ctx, env) {
5421
+ const leading = leadingCommands(moduleScripts(ctx.cwd)[LINT_SCRIPT_NAME]);
5422
+ if (leading === void 0) return true;
5423
+ const result = spawnSync4(leading, {
5424
+ cwd: ctx.cwd,
5425
+ shell: true,
5426
+ encoding: "utf8",
5427
+ env: { ...env, PATH: scriptPath(ctx.cwd, env.PATH) },
5428
+ maxBuffer: CAPTURE_MAX_BUFFER
5429
+ });
5430
+ if (!result.error && result.status === 0) {
5431
+ process.stderr.write(
5432
+ ` ran \`${leading}\` before measuring, as the lint script runs it before sentinel
5433
+ `
5434
+ );
5435
+ return true;
5436
+ }
5437
+ const said = (result.error?.message ?? `${result.stderr ?? ""}${result.stdout ?? ""}`).trim().split("\n").slice(-1)[0];
5438
+ process.stderr.write(
5439
+ ` ${palette(process.stderr).warn(
5440
+ `the lint script runs \`${leading}\` before sentinel, and it ${result.error ? "could not start" : `exited ${result.status ?? 1}`} here${said ? ` (${said})` : ""}, so this module's existing violations were not measured and ${LINT_BASELINE_FILE} was left as it was. Fix that, then run \`sentinel --init --lint\` again.`
5441
+ )}
5442
+ `
5443
+ );
5444
+ return false;
5445
+ }
5446
+ /**
5447
+ * The fix pass alone, in the copy `--init --dry-run` hands it, without the baseline measurement
5448
+ * that follows it for real. See `Adapter.rehearseAfterInit`.
5449
+ */
5450
+ async rehearseAfterInit(ctx, origin) {
5451
+ const oxlint = resolveOxlint(origin);
5452
+ if (!oxlint) return "oxlint is not installed in this module yet";
5453
+ const env = { ...process.env, PATH: oxlintPath(origin, process.env) };
5454
+ return this.fixPass(ctx, oxlint, env).failure;
5043
5455
  }
5044
5456
  /**
5045
5457
  * Measure what THIS module already violates, and hold exactly those rules at `warn`.
@@ -5059,23 +5471,23 @@ ${result.stderr ?? ""}`);
5059
5471
  * build the developer can see, rather than a silent half-adoption they cannot.
5060
5472
  */
5061
5473
  writeModuleBaseline(ctx, oxlint, env) {
5062
- const configPath = join16(ctx.cwd, LINT_CONFIG_FILE);
5063
- const baselinePath = join16(ctx.cwd, LINT_BASELINE_FILE);
5474
+ const configPath = join17(ctx.cwd, LINT_CONFIG_FILE);
5475
+ const baselinePath = join17(ctx.cwd, LINT_BASELINE_FILE);
5064
5476
  if (!existsSync12(configPath)) return;
5065
5477
  let config;
5066
5478
  try {
5067
5479
  config = parseJsonc(
5068
- readFileSync13(configPath, "utf8"),
5480
+ readFileSync14(configPath, "utf8"),
5069
5481
  LINT_CONFIG_FILE
5070
5482
  );
5071
5483
  } catch {
5072
5484
  return;
5073
5485
  }
5074
5486
  const current = Array.isArray(config.extends) ? config.extends : [];
5075
- const measurePath = join16(ctx.cwd, LINT_MEASURE_FILE);
5487
+ const measurePath = join17(ctx.cwd, LINT_MEASURE_FILE);
5076
5488
  let measured;
5077
5489
  try {
5078
- writeFileSync2(
5490
+ writeFileSync3(
5079
5491
  measurePath,
5080
5492
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
5081
5493
  `
@@ -5107,7 +5519,7 @@ ${result.stderr ?? ""}`);
5107
5519
  const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
5108
5520
  const next = extendsWithBaseline(current, holds.length > 0);
5109
5521
  if (holds.length > 0) {
5110
- writeFileSync2(baselinePath, renderBaseline(holds, readOwnPackage().version));
5522
+ writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
5111
5523
  process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
5112
5524
  `);
5113
5525
  } else {
@@ -5115,12 +5527,12 @@ ${result.stderr ?? ""}`);
5115
5527
  }
5116
5528
  if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
5117
5529
  config.extends = next;
5118
- writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
5530
+ writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
5119
5531
  `);
5120
5532
  }
5121
5533
  }
5122
5534
  async run(ctx) {
5123
- if (!existsSync12(join16(ctx.cwd, LINT_CONFIG_FILE))) {
5535
+ if (!existsSync12(join17(ctx.cwd, LINT_CONFIG_FILE))) {
5124
5536
  process.stderr.write(
5125
5537
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
5126
5538
  `
@@ -5268,7 +5680,7 @@ ${result.stderr ?? ""}`);
5268
5680
  let stub;
5269
5681
  try {
5270
5682
  stub = parseJsonc(
5271
- readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
5683
+ readFileSync14(join17(cwd, LINT_CONFIG_FILE), "utf8"),
5272
5684
  LINT_CONFIG_FILE
5273
5685
  );
5274
5686
  } catch {
@@ -5278,7 +5690,7 @@ ${result.stderr ?? ""}`);
5278
5690
  for (const entry of stub.extends ?? []) {
5279
5691
  try {
5280
5692
  const preset = parseJsonc(
5281
- readFileSync13(join16(cwd, entry), "utf8"),
5693
+ readFileSync14(join17(cwd, entry), "utf8"),
5282
5694
  entry
5283
5695
  );
5284
5696
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -5348,14 +5760,14 @@ ${result.stderr ?? ""}`);
5348
5760
  let parsed;
5349
5761
  try {
5350
5762
  parsed = parseJsonc(
5351
- readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
5763
+ readFileSync14(join17(cwd, LINT_CONFIG_FILE), "utf8"),
5352
5764
  LINT_CONFIG_FILE
5353
5765
  );
5354
5766
  } catch {
5355
5767
  return void 0;
5356
5768
  }
5357
5769
  const targets2 = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
5358
- return targets2.find((target) => !existsSync12(join16(cwd, target)));
5770
+ return targets2.find((target) => !existsSync12(join17(cwd, target)));
5359
5771
  }
5360
5772
  /** Announce what is not enforced, so reduced coverage is never silent. */
5361
5773
  announceDisabled(preset) {
@@ -5387,8 +5799,8 @@ function registerLint() {
5387
5799
 
5388
5800
  // src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
5389
5801
  import { spawnSync as spawnSync6 } from "node:child_process";
5390
- import { existsSync as existsSync18, mkdirSync as mkdirSync2, readFileSync as readFileSync19, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
5391
- import { join as join26 } from "node:path";
5802
+ import { existsSync as existsSync18, mkdirSync as mkdirSync2, readFileSync as readFileSync20, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
5803
+ import { join as join27 } from "node:path";
5392
5804
  import { globSync as globSync5 } from "tinyglobby";
5393
5805
 
5394
5806
  // src/roles/static-analysis/bundle/budgets.ts
@@ -5417,21 +5829,21 @@ function bandFor(valueKb, referenceKb) {
5417
5829
  }
5418
5830
 
5419
5831
  // src/roles/static-analysis/bundle/scan.ts
5420
- import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
5421
- import { basename, join as join18 } from "node:path";
5832
+ import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
5833
+ import { basename, join as join19 } from "node:path";
5422
5834
  import { pathToFileURL } from "node:url";
5423
5835
  import { gzipSync } from "node:zlib";
5424
5836
  import { globSync as globSync2 } from "tinyglobby";
5425
5837
 
5426
5838
  // src/roles/static-analysis/bundle/find-build.ts
5427
- import { existsSync as existsSync13, readFileSync as readFileSync14, statSync as statSync4 } from "node:fs";
5428
- import { join as join17 } from "node:path";
5839
+ import { existsSync as existsSync13, readFileSync as readFileSync15, statSync as statSync4 } from "node:fs";
5840
+ import { join as join18 } from "node:path";
5429
5841
  import { globSync } from "tinyglobby";
5430
5842
  var MANIFEST_GLOB = "server/_tanstack-start-manifest*.mjs";
5431
5843
  function candidateDirs(cwd) {
5432
5844
  const declared = readProjectPackageJson(cwd).nx?.targets?.build?.["outputs"];
5433
- const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) => join17(cwd, token.replace(/^\{projectRoot\}\//, ""))) : [];
5434
- return [...fromDeclaration, join17(cwd, ".output"), join17(cwd, "dist"), join17(cwd, "build")];
5845
+ const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) => join18(cwd, token.replace(/^\{projectRoot\}\//, ""))) : [];
5846
+ return [...fromDeclaration, join18(cwd, ".output"), join18(cwd, "dist"), join18(cwd, "build")];
5435
5847
  }
5436
5848
  function findBuildOutput(cwd) {
5437
5849
  const lookedIn = [];
@@ -5441,7 +5853,7 @@ function findBuildOutput(cwd) {
5441
5853
  const found = globSync(MANIFEST_GLOB, { cwd: dir, absolute: true, dot: false });
5442
5854
  if (found.length === 0) continue;
5443
5855
  const manifestPath = found[0];
5444
- const publicDir = join17(dir, "public");
5856
+ const publicDir = join18(dir, "public");
5445
5857
  if (!existsSync13(publicDir)) continue;
5446
5858
  return { manifestPath, publicDir, ...dateOf(dir, manifestPath) };
5447
5859
  }
@@ -5464,10 +5876,10 @@ function findBuildOutput(cwd) {
5464
5876
  };
5465
5877
  }
5466
5878
  function dateOf(dir, manifestPath) {
5467
- const record = join17(dir, "nitro.json");
5879
+ const record = join18(dir, "nitro.json");
5468
5880
  if (existsSync13(record)) {
5469
5881
  try {
5470
- const parsed = JSON.parse(readFileSync14(record, "utf8"));
5882
+ const parsed = JSON.parse(readFileSync15(record, "utf8"));
5471
5883
  if (typeof parsed.date === "string") {
5472
5884
  const date = new Date(parsed.date);
5473
5885
  if (!Number.isNaN(date.getTime())) return { builtAt: date, builtAtFrom: "build record" };
@@ -5493,9 +5905,9 @@ function kindOf(path2) {
5493
5905
  return "other";
5494
5906
  }
5495
5907
  function resolveAnnounced(publicDir, announced) {
5496
- const direct = join18(publicDir, announced);
5908
+ const direct = join19(publicDir, announced);
5497
5909
  if (existsSync14(direct)) return direct;
5498
- const stripped = join18(publicDir, announced.replace(/^\/[^/]+\//, "/"));
5910
+ const stripped = join19(publicDir, announced.replace(/^\/[^/]+\//, "/"));
5499
5911
  return existsSync14(stripped) ? stripped : void 0;
5500
5912
  }
5501
5913
  function emptyWeight() {
@@ -5532,7 +5944,7 @@ async function scanBundle(cwd) {
5532
5944
  unresolved.push(announced);
5533
5945
  measured.set(announced, void 0);
5534
5946
  } else {
5535
- const buffer = readFileSync15(path2);
5947
+ const buffer = readFileSync16(path2);
5536
5948
  measured.set(announced, {
5537
5949
  parsed: buffer.length,
5538
5950
  transfer: gzipSync(buffer, { level: 9 }).length
@@ -5619,7 +6031,7 @@ function scanDynamicLayer(publicDir, references) {
5619
6031
  })) {
5620
6032
  const name = basename(path2);
5621
6033
  if (referenced.has(name)) continue;
5622
- const buffer = readFileSync15(path2);
6034
+ const buffer = readFileSync16(path2);
5623
6035
  const size = { parsed: buffer.length, transfer: gzipSync(buffer, { level: 9 }).length };
5624
6036
  add(all2, size);
5625
6037
  const group = groupOf(name);
@@ -5647,23 +6059,23 @@ function scanDynamicLayer(publicDir, references) {
5647
6059
  }
5648
6060
 
5649
6061
  // src/roles/static-analysis/sources.ts
5650
- import { readFileSync as readFileSync16 } from "node:fs";
5651
- import { join as join21 } from "node:path";
5652
- import { parseSync as parseSync2 } from "oxc-parser";
6062
+ import { readFileSync as readFileSync17 } from "node:fs";
6063
+ import { join as join22 } from "node:path";
6064
+ import { parseSync as parseSync3 } from "oxc-parser";
5653
6065
  import { globSync as globSync3 } from "tinyglobby";
5654
6066
 
5655
6067
  // src/roles/static-analysis/adapters/jscpd/tool-config.ts
5656
- import { existsSync as existsSync16, mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
6068
+ import { existsSync as existsSync16, mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
5657
6069
  import { tmpdir } from "node:os";
5658
- import { join as join20 } from "node:path";
6070
+ import { join as join21 } from "node:path";
5659
6071
 
5660
6072
  // src/roles/static-analysis/configs/resolve.ts
5661
6073
  import { existsSync as existsSync15 } from "node:fs";
5662
- import { join as join19 } from "node:path";
6074
+ import { join as join20 } from "node:path";
5663
6075
  import { pathToFileURL as pathToFileURL2 } from "node:url";
5664
6076
  var STATIC_CONFIG_FILE = "static-analysis.config.mjs";
5665
6077
  async function loadStaticConfig(cwd) {
5666
- const path2 = join19(cwd, STATIC_CONFIG_FILE);
6078
+ const path2 = join20(cwd, STATIC_CONFIG_FILE);
5667
6079
  if (!existsSync15(path2)) {
5668
6080
  return { config: defineStaticAnalysis(), from: null, unreadable: null };
5669
6081
  }
@@ -5710,7 +6122,7 @@ function exclusions(config, scope = "tests") {
5710
6122
  return [.../* @__PURE__ */ new Set([...config.ignore, ...OWN_ARTEFACTS, ...tests])];
5711
6123
  }
5712
6124
  function measuredRoots(cwd) {
5713
- return existsSync16(join20(cwd, "src")) ? ["src"] : ["."];
6125
+ return existsSync16(join21(cwd, "src")) ? ["src"] : ["."];
5714
6126
  }
5715
6127
  function fileSettings(config, scope, extraEntries = []) {
5716
6128
  return {
@@ -5740,9 +6152,9 @@ function fileSettings(config, scope, extraEntries = []) {
5740
6152
  };
5741
6153
  }
5742
6154
  function duplicationRun(config, paths, scope) {
5743
- const dir = mkdtempSync(join20(tmpdir(), "sentinel-static-"));
5744
- const configPath = join20(dir, "jscpd.json");
5745
- writeFileSync3(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
6155
+ const dir = mkdtempSync(join21(tmpdir(), "sentinel-static-"));
6156
+ const configPath = join21(dir, "jscpd.json");
6157
+ writeFileSync4(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
5746
6158
  `, "utf8");
5747
6159
  const args = [
5748
6160
  "--config",
@@ -5767,9 +6179,9 @@ function duplicationRun(config, paths, scope) {
5767
6179
  return { args, rawReportDir: dir, dispose: () => rmSync3(dir, { force: true, recursive: true }) };
5768
6180
  }
5769
6181
  function deadCodeRun(config, paths, extraEntries = []) {
5770
- const dir = mkdtempSync(join20(tmpdir(), "sentinel-static-"));
5771
- const configPath = join20(dir, "jscpd.json");
5772
- writeFileSync3(
6182
+ const dir = mkdtempSync(join21(tmpdir(), "sentinel-static-"));
6183
+ const configPath = join21(dir, "jscpd.json");
6184
+ writeFileSync4(
5773
6185
  configPath,
5774
6186
  `${JSON.stringify(fileSettings(config, "dead-code", extraEntries), null, 2)}
5775
6187
  `,
@@ -5820,12 +6232,12 @@ function readParsed(cwd, files, passes) {
5820
6232
  function parsedSource(cwd, path2) {
5821
6233
  let text;
5822
6234
  try {
5823
- text = readFileSync16(join21(cwd, path2), "utf8");
6235
+ text = readFileSync17(join22(cwd, path2), "utf8");
5824
6236
  } catch {
5825
6237
  return void 0;
5826
6238
  }
5827
6239
  try {
5828
- const parsed = parseSync2(path2, text);
6240
+ const parsed = parseSync3(path2, text);
5829
6241
  if (parsed.errors.length > 0) return void 0;
5830
6242
  return { path: path2, text, program: parsed.program };
5831
6243
  } catch {
@@ -5834,7 +6246,7 @@ function parsedSource(cwd, path2) {
5834
6246
  }
5835
6247
 
5836
6248
  // src/roles/static-analysis/complexity/cyclomatic.ts
5837
- import { parseSync as parseSync3 } from "oxc-parser";
6249
+ import { parseSync as parseSync4 } from "oxc-parser";
5838
6250
  var FUNCTION_NODES = /* @__PURE__ */ new Set([
5839
6251
  "FunctionDeclaration",
5840
6252
  "FunctionExpression",
@@ -6007,13 +6419,13 @@ function unreadableNote(coverage) {
6007
6419
  }
6008
6420
 
6009
6421
  // src/roles/static-analysis/excerpt.ts
6010
- import { readFileSync as readFileSync17 } from "node:fs";
6011
- import { join as join23 } from "node:path";
6422
+ import { readFileSync as readFileSync18 } from "node:fs";
6423
+ import { join as join24 } from "node:path";
6012
6424
 
6013
6425
  // src/roles/static-analysis/maintainability/size-rules.ts
6014
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
6426
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
6015
6427
  import { tmpdir as tmpdir2 } from "node:os";
6016
- import { join as join22 } from "node:path";
6428
+ import { join as join23 } from "node:path";
6017
6429
  var GAUGE_THRESHOLDS = {
6018
6430
  // McCabe 1976 says 10, our p90 says 11. The only indicator where two independent candidates agree
6019
6431
  // within one unit, and 94.8% of our functions already sit in the 1-10 band SEI calls low risk.
@@ -6042,7 +6454,7 @@ var GAUGE_THRESHOLDS = {
6042
6454
  "cognitive-complexity": 7
6043
6455
  };
6044
6456
  var COGNITIVE = "cognitive-complexity";
6045
- var cognitivePlugin = () => join22(ownPackageDir(), "plugins", "sonar-cognitive.js");
6457
+ var cognitivePlugin = () => join23(ownPackageDir(), "plugins", "sonar-cognitive.js");
6046
6458
  var CATEGORIES = [
6047
6459
  "correctness",
6048
6460
  "suspicious",
@@ -6074,9 +6486,9 @@ function sizeRulesConfig() {
6074
6486
  };
6075
6487
  }
6076
6488
  function sizeRulesRun(paths, ignore) {
6077
- const dir = mkdtempSync2(join22(tmpdir2(), "sentinel-size-"));
6078
- const configPath = join22(dir, "oxlintrc.json");
6079
- writeFileSync4(configPath, `${JSON.stringify(sizeRulesConfig(), null, 2)}
6489
+ const dir = mkdtempSync2(join23(tmpdir2(), "sentinel-size-"));
6490
+ const configPath = join23(dir, "oxlintrc.json");
6491
+ writeFileSync5(configPath, `${JSON.stringify(sizeRulesConfig(), null, 2)}
6080
6492
  `, "utf8");
6081
6493
  return {
6082
6494
  // `--format json` because the console shape is rendered by this role from the figures: a
@@ -7533,7 +7945,7 @@ function linesOf(cache, cwd, relative5) {
7533
7945
  if (cache.has(relative5)) return cache.get(relative5);
7534
7946
  let lines;
7535
7947
  try {
7536
- lines = readFileSync17(join23(cwd, relative5), "utf8").split("\n");
7948
+ lines = readFileSync18(join24(cwd, relative5), "utf8").split("\n");
7537
7949
  } catch {
7538
7950
  lines = void 0;
7539
7951
  }
@@ -7765,7 +8177,7 @@ var SCRIPT = `(function(){
7765
8177
  })();`;
7766
8178
 
7767
8179
  // src/roles/static-analysis/graph/imports.ts
7768
- import { dirname, join as join24 } from "node:path";
8180
+ import { dirname, join as join25 } from "node:path";
7769
8181
  var TS_BEHIND_JS = {
7770
8182
  ".js": [".ts", ".tsx"],
7771
8183
  ".mjs": [".mts"],
@@ -7818,7 +8230,7 @@ function specifiersOf(program) {
7818
8230
  }
7819
8231
  function resolveSpecifier(from, specifier, known) {
7820
8232
  if (!specifier.startsWith(".")) return void 0;
7821
- const base = join24(dirname(from), specifier);
8233
+ const base = join25(dirname(from), specifier);
7822
8234
  if (known.has(base)) return base;
7823
8235
  for (const extension of RESOLVABLE_EXTENSIONS) {
7824
8236
  if (known.has(base + extension)) return base + extension;
@@ -7829,7 +8241,7 @@ function resolveSpecifier(from, specifier, known) {
7829
8241
  if (known.has(swapped)) return swapped;
7830
8242
  }
7831
8243
  for (const extension of RESOLVABLE_EXTENSIONS) {
7832
- const index = join24(base, `index${extension}`);
8244
+ const index = join25(base, `index${extension}`);
7833
8245
  if (known.has(index)) return index;
7834
8246
  }
7835
8247
  return void 0;
@@ -7942,26 +8354,26 @@ function scanSyntax(cwd, config) {
7942
8354
 
7943
8355
  // src/roles/static-analysis/workspace/consumers.ts
7944
8356
  import { spawnSync as spawnSync5 } from "node:child_process";
7945
- import { existsSync as existsSync17, mkdirSync, readFileSync as readFileSync18, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
7946
- import { dirname as dirname2, join as join25, relative as relative4, resolve as resolve3, sep } from "node:path";
7947
- var CACHE_DIR = join25("node_modules", ".cache", "sentinel");
8357
+ import { existsSync as existsSync17, mkdirSync, readFileSync as readFileSync19, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
8358
+ import { dirname as dirname2, join as join26, relative as relative4, resolve as resolve3, sep } from "node:path";
8359
+ var CACHE_DIR = join26("node_modules", ".cache", "sentinel");
7948
8360
  var CACHE_FILE = "consumer-index.json";
7949
8361
  var SHAPE = 4;
7950
8362
  function workspaceRootFrom(cwd) {
7951
8363
  let dir = resolve3(cwd);
7952
8364
  for (; ; ) {
7953
- if (existsSync17(join25(dir, WORKSPACE_ROOT_MARKER))) return dir;
8365
+ if (existsSync17(join26(dir, WORKSPACE_ROOT_MARKER))) return dir;
7954
8366
  const parent = dirname2(dir);
7955
8367
  if (parent === dir) return void 0;
7956
8368
  dir = parent;
7957
8369
  }
7958
8370
  }
7959
8371
  function aliasTable(workspaceRoot) {
7960
- const file = join25(workspaceRoot, "tsconfig.base.json");
8372
+ const file = join26(workspaceRoot, "tsconfig.base.json");
7961
8373
  if (!existsSync17(file)) return /* @__PURE__ */ new Map();
7962
8374
  let paths;
7963
8375
  try {
7964
- const parsed = JSON.parse(readFileSync18(file, "utf8"));
8376
+ const parsed = JSON.parse(readFileSync19(file, "utf8"));
7965
8377
  paths = parsed.compilerOptions?.paths ?? {};
7966
8378
  } catch {
7967
8379
  return /* @__PURE__ */ new Map();
@@ -7979,7 +8391,7 @@ function aliasTable(workspaceRoot) {
7979
8391
  function resolveSpecifier2(specifier, table, workspaceRoot) {
7980
8392
  const isFile = (candidate) => {
7981
8393
  try {
7982
- return statSync5(join25(workspaceRoot, candidate)).isFile();
8394
+ return statSync5(join26(workspaceRoot, candidate)).isFile();
7983
8395
  } catch {
7984
8396
  return false;
7985
8397
  }
@@ -8016,11 +8428,11 @@ function resolveSpecifier2(specifier, table, workspaceRoot) {
8016
8428
  return void 0;
8017
8429
  }
8018
8430
  function resolveRelative(importer, specifier, workspaceRoot) {
8019
- const base = join25(dirname2(importer), specifier).split(sep).join("/");
8431
+ const base = join26(dirname2(importer), specifier).split(sep).join("/");
8020
8432
  if (base.startsWith("..")) return void 0;
8021
8433
  const isFile = (candidate) => {
8022
8434
  try {
8023
- return statSync5(join25(workspaceRoot, candidate)).isFile();
8435
+ return statSync5(join26(workspaceRoot, candidate)).isFile();
8024
8436
  } catch {
8025
8437
  return false;
8026
8438
  }
@@ -8118,10 +8530,10 @@ function hash(text) {
8118
8530
  }
8119
8531
  function consumerIndex(workspaceRoot, moduleRoots) {
8120
8532
  const key = cacheKey(workspaceRoot);
8121
- const file = join25(workspaceRoot, CACHE_DIR, CACHE_FILE);
8533
+ const file = join26(workspaceRoot, CACHE_DIR, CACHE_FILE);
8122
8534
  if (key && existsSync17(file)) {
8123
8535
  try {
8124
- const cached = JSON.parse(readFileSync18(file, "utf8"));
8536
+ const cached = JSON.parse(readFileSync19(file, "utf8"));
8125
8537
  if (cached.key === key) {
8126
8538
  return {
8127
8539
  imported: new Set(cached.imported),
@@ -8135,14 +8547,14 @@ function consumerIndex(workspaceRoot, moduleRoots) {
8135
8547
  const index = buildConsumerIndex(workspaceRoot, moduleRoots());
8136
8548
  if (key) {
8137
8549
  try {
8138
- mkdirSync(join25(workspaceRoot, CACHE_DIR), { recursive: true });
8550
+ mkdirSync(join26(workspaceRoot, CACHE_DIR), { recursive: true });
8139
8551
  const payload = {
8140
8552
  key,
8141
8553
  imported: [...index.imported],
8142
8554
  testOnly: [...index.testOnly],
8143
8555
  builtFrom: index.builtFrom
8144
8556
  };
8145
- writeFileSync5(file, JSON.stringify(payload), "utf8");
8557
+ writeFileSync6(file, JSON.stringify(payload), "utf8");
8146
8558
  } catch {
8147
8559
  }
8148
8560
  }
@@ -8239,7 +8651,7 @@ var JscpdAdapter = class extends BaseAdapter {
8239
8651
  operations: [
8240
8652
  // `write` and not `merge-json`: this file is code, and the engine's in-place merge is for
8241
8653
  // JSON. A re-init therefore RESTORES the stub, so an existing one is left alone below.
8242
- ...existsSync18(join26(context2.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: configStub() }],
8654
+ ...existsSync18(join27(context2.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: configStub() }],
8243
8655
  manifestOperation(context2.cwd, scripts()),
8244
8656
  ...nxTargetOperations({ cwd: context2.cwd, targets: targets() })
8245
8657
  // The report directory is ignored ONCE at the workspace root, by `--init`'s workspace prep,
@@ -8489,7 +8901,7 @@ var JscpdAdapter = class extends BaseAdapter {
8489
8901
  const declared = moduleScripts(ctx.cwd);
8490
8902
  const missing = Object.keys(scripts()).filter((name) => !(name in declared));
8491
8903
  const adopted = missing.length < Object.keys(scripts()).length;
8492
- const hasConfig = existsSync18(join26(ctx.cwd, STATIC_CONFIG_FILE));
8904
+ const hasConfig = existsSync18(join27(ctx.cwd, STATIC_CONFIG_FILE));
8493
8905
  return {
8494
8906
  adopted,
8495
8907
  preset: adopted ? ctx.preset : null,
@@ -8581,7 +8993,7 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
8581
8993
  try {
8582
8994
  const ran = runTool(jscpd, args, cwd, "dead-code");
8583
8995
  if (ran.kind !== "ran") return ran;
8584
- const path2 = join26(rawReportDir, RAW_REPORT["dead-code"]);
8996
+ const path2 = join27(rawReportDir, RAW_REPORT["dead-code"]);
8585
8997
  if (!existsSync18(path2)) {
8586
8998
  process.stderr.write(
8587
8999
  `sentinel static-analysis(jscpd): the dead-code pass wrote no ${RAW_REPORT["dead-code"]}
@@ -8589,7 +9001,7 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
8589
9001
  );
8590
9002
  return { kind: "failed", code: 1 };
8591
9003
  }
8592
- const parsed = safeParseDeadCode(readFileSync19(path2, "utf8"));
9004
+ const parsed = safeParseDeadCode(readFileSync20(path2, "utf8"));
8593
9005
  const statistics = parsed?.statistics;
8594
9006
  if (!statistics) {
8595
9007
  process.stderr.write(
@@ -8612,11 +9024,11 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
8612
9024
  }
8613
9025
  }
8614
9026
  function moduleRelative(name, roots, cwd) {
8615
- if (existsSync18(join26(cwd, name))) return name;
9027
+ if (existsSync18(join27(cwd, name))) return name;
8616
9028
  for (const root of roots) {
8617
9029
  if (root === ".") continue;
8618
9030
  const candidate = `${root}/${name}`;
8619
- if (existsSync18(join26(cwd, candidate))) return candidate;
9031
+ if (existsSync18(join27(cwd, candidate))) return candidate;
8620
9032
  }
8621
9033
  return name;
8622
9034
  }
@@ -8625,8 +9037,8 @@ function runDuplication(jscpd, config, paths, scope, cwd) {
8625
9037
  try {
8626
9038
  const ran = runTool(jscpd, args, cwd, scope);
8627
9039
  if (ran.kind !== "ran") return ran;
8628
- const path2 = join26(rawReportDir, RAW_REPORT.duplication);
8629
- const parsed = existsSync18(path2) ? safeParse(readFileSync19(path2, "utf8")) : void 0;
9040
+ const path2 = join27(rawReportDir, RAW_REPORT.duplication);
9041
+ const parsed = existsSync18(path2) ? safeParse(readFileSync20(path2, "utf8")) : void 0;
8630
9042
  const total = parsed?.statistics?.total;
8631
9043
  if (!total) {
8632
9044
  process.stderr.write(
@@ -8795,7 +9207,7 @@ function outsideOf(cwd) {
8795
9207
  function moduleRootsOf(workspaceRoot) {
8796
9208
  const roots = [];
8797
9209
  for (const area of ["apps", "libs"]) {
8798
- const base = join26(workspaceRoot, area);
9210
+ const base = join27(workspaceRoot, area);
8799
9211
  if (!existsSync18(base)) continue;
8800
9212
  for (const entry of globSync5([`${area}/**/package.json`, `${area}/**/project.json`], {
8801
9213
  cwd: workspaceRoot,
@@ -9138,7 +9550,7 @@ var PICTURE_FILE = `${TARGET}-graph.svg`;
9138
9550
  function clearStaleOutputs(cwd, keeping) {
9139
9551
  for (const name of [...Object.values(SHAPE_FILE), PICTURE_FILE]) {
9140
9552
  if (keeping.has(name)) continue;
9141
- const path2 = join26(cwd, REPORT_DIR, name);
9553
+ const path2 = join27(cwd, REPORT_DIR, name);
9142
9554
  if (existsSync18(path2)) unlinkSync(path2);
9143
9555
  }
9144
9556
  }
@@ -9157,11 +9569,11 @@ function drawsPicture(graph, coverage) {
9157
9569
  async function emit(report, shape, cwd, graph, coverage) {
9158
9570
  if (shape === "console") return [];
9159
9571
  const detailed = shape === "markdown" ? report : withExcerpts(report, cwd);
9160
- mkdirSync2(join26(cwd, REPORT_DIR), { recursive: true });
9572
+ mkdirSync2(join27(cwd, REPORT_DIR), { recursive: true });
9161
9573
  const written = [];
9162
9574
  if (shape === "html" && drawsPicture(graph, coverage)) {
9163
9575
  const drawn = layOut(graph.graph, graph.communityOf);
9164
- const path2 = join26(cwd, REPORT_DIR, PICTURE_FILE);
9576
+ const path2 = join27(cwd, REPORT_DIR, PICTURE_FILE);
9165
9577
  await writeGraphPicture(drawn, path2);
9166
9578
  written.push(`${REPORT_DIR}/${PICTURE_FILE}`);
9167
9579
  detailed.picture = buildExplorer(drawn, graph.communityOf).markup;
@@ -9169,7 +9581,7 @@ async function emit(report, shape, cwd, graph, coverage) {
9169
9581
  const RENDER = { json: renderJson, html: renderHtml, markdown: renderMarkdown };
9170
9582
  const name = SHAPE_FILE[shape] ?? `${TARGET}.${shape}`;
9171
9583
  const body = RENDER[shape](detailed);
9172
- writeFileSync6(join26(cwd, REPORT_DIR, name), body, "utf8");
9584
+ writeFileSync7(join27(cwd, REPORT_DIR, name), body, "utf8");
9173
9585
  written.unshift(`${REPORT_DIR}/${name}`);
9174
9586
  return written;
9175
9587
  }
@@ -9203,9 +9615,9 @@ function registerTest() {
9203
9615
 
9204
9616
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
9205
9617
  import { spawnSync as spawnSync7 } from "node:child_process";
9206
- import { existsSync as existsSync19, readFileSync as readFileSync21 } from "node:fs";
9618
+ import { existsSync as existsSync19, readFileSync as readFileSync22 } from "node:fs";
9207
9619
  import { createRequire } from "node:module";
9208
- import { join as join28 } from "node:path";
9620
+ import { join as join29 } from "node:path";
9209
9621
 
9210
9622
  // src/roles/typescript/presets/nest.json
9211
9623
  var nest_default2 = {
@@ -9390,8 +9802,8 @@ function readTsconfigAdoption(cwd) {
9390
9802
  }
9391
9803
 
9392
9804
  // src/roles/typescript/adapters/tsc/plan.ts
9393
- import { readFileSync as readFileSync20 } from "node:fs";
9394
- import { join as join27 } from "node:path";
9805
+ import { readFileSync as readFileSync21 } from "node:fs";
9806
+ import { join as join28 } from "node:path";
9395
9807
 
9396
9808
  // src/roles/typescript/typecheck-script.ts
9397
9809
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -9484,7 +9896,7 @@ function planAdoption(context2) {
9484
9896
  };
9485
9897
  }
9486
9898
  const existing = parseJsonc(
9487
- readFileSync20(join27(context2.cwd, target.path), "utf8"),
9899
+ readFileSync21(join28(context2.cwd, target.path), "utf8"),
9488
9900
  target.path
9489
9901
  );
9490
9902
  const extendsChain = composeExtends(existing.extends, preset);
@@ -9622,7 +10034,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9622
10034
  let chain;
9623
10035
  try {
9624
10036
  const parsed = parseJsonc(
9625
- readFileSync21(join28(cwd, target.path), "utf8"),
10037
+ readFileSync22(join29(cwd, target.path), "utf8"),
9626
10038
  target.path
9627
10039
  );
9628
10040
  chain = parsed.extends;
@@ -9635,7 +10047,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9635
10047
  );
9636
10048
  if (preset === void 0) return void 0;
9637
10049
  try {
9638
- createRequire(join28(cwd, "noop.js")).resolve(preset);
10050
+ createRequire(join29(cwd, "noop.js")).resolve(preset);
9639
10051
  return void 0;
9640
10052
  } catch {
9641
10053
  return preset;
@@ -9731,7 +10143,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9731
10143
  referencedProjects(cwd, config) {
9732
10144
  try {
9733
10145
  const parsed = parseJsonc(
9734
- readFileSync21(join28(cwd, config), "utf8"),
10146
+ readFileSync22(join29(cwd, config), "utf8"),
9735
10147
  config
9736
10148
  );
9737
10149
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path2) => typeof path2 === "string" && path2.length > 0);
@@ -9747,7 +10159,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9747
10159
  * check.
9748
10160
  */
9749
10161
  typecheckTarget(cwd) {
9750
- if (existsSync19(join28(cwd, "tsconfig.json"))) return "tsconfig.json";
10162
+ if (existsSync19(join29(cwd, "tsconfig.json"))) return "tsconfig.json";
9751
10163
  const target = resolveTsconfigTarget(cwd);
9752
10164
  return target.reason === "none" ? null : target.path;
9753
10165
  }
@@ -9953,7 +10365,7 @@ function replaceLines(current, replacements) {
9953
10365
  }
9954
10366
 
9955
10367
  // src/core/apply-plan.ts
9956
- import { existsSync as existsSync20, readFileSync as readFileSync22, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
10368
+ import { existsSync as existsSync20, readFileSync as readFileSync23, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
9957
10369
  import { resolve as resolve4, sep as sep2 } from "node:path";
9958
10370
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
9959
10371
  function resolveWithinRoot(cwd, relativePath) {
@@ -9965,7 +10377,7 @@ function resolveWithinRoot(cwd, relativePath) {
9965
10377
  return absolutePath;
9966
10378
  }
9967
10379
  function readIfExists(absolutePath) {
9968
- return existsSync20(absolutePath) ? readFileSync22(absolutePath, "utf8") : void 0;
10380
+ return existsSync20(absolutePath) ? readFileSync23(absolutePath, "utf8") : void 0;
9969
10381
  }
9970
10382
  function* leaves(value, prefix = []) {
9971
10383
  for (const [key, keyValue] of Object.entries(value)) {
@@ -10067,7 +10479,7 @@ function preparePlan(cwd, plan3) {
10067
10479
  }
10068
10480
  function writeFileAtomic(absolutePath, contents) {
10069
10481
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
10070
- writeFileSync7(tempPath, contents);
10482
+ writeFileSync8(tempPath, contents);
10071
10483
  renameSync(tempPath, absolutePath);
10072
10484
  }
10073
10485
  function applyPlan(cwd, plan3) {
@@ -10083,8 +10495,8 @@ function applyPlan(cwd, plan3) {
10083
10495
  }
10084
10496
 
10085
10497
  // src/core/config/preset-evidence.ts
10086
- import { existsSync as existsSync21, readdirSync as readdirSync3, readFileSync as readFileSync23 } from "node:fs";
10087
- import { join as join29 } from "node:path";
10498
+ import { existsSync as existsSync21, readdirSync as readdirSync3, readFileSync as readFileSync24 } from "node:fs";
10499
+ import { join as join30 } from "node:path";
10088
10500
  var PATH_SIGNALS = [
10089
10501
  {
10090
10502
  preset: "nest",
@@ -10118,12 +10530,12 @@ function sourceImports(cwd, pattern) {
10118
10530
  }
10119
10531
  for (const entry of entries) {
10120
10532
  if (entry.isDirectory()) {
10121
- if (!NOT_SCANNED.has(entry.name)) stack.push(join29(dir, entry.name));
10533
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join30(dir, entry.name));
10122
10534
  continue;
10123
10535
  }
10124
10536
  if (!SOURCE_FILE.test(entry.name)) continue;
10125
10537
  try {
10126
- if (pattern.test(readFileSync23(join29(dir, entry.name), "utf8"))) return true;
10538
+ if (pattern.test(readFileSync24(join30(dir, entry.name), "utf8"))) return true;
10127
10539
  } catch {
10128
10540
  continue;
10129
10541
  }
@@ -10132,11 +10544,11 @@ function sourceImports(cwd, pattern) {
10132
10544
  return false;
10133
10545
  }
10134
10546
  function dependencyNames(cwd) {
10135
- const path2 = join29(cwd, "package.json");
10547
+ const path2 = join30(cwd, "package.json");
10136
10548
  if (!existsSync21(path2)) return [];
10137
10549
  try {
10138
10550
  const manifest = parseJsonc(
10139
- readFileSync23(path2, "utf8"),
10551
+ readFileSync24(path2, "utf8"),
10140
10552
  path2
10141
10553
  );
10142
10554
  return [
@@ -10159,7 +10571,7 @@ function reactTsconfigEvidence(cwd) {
10159
10571
  }
10160
10572
  for (const name of entries) {
10161
10573
  try {
10162
- const config = parseJsonc(readFileSync23(join29(cwd, name), "utf8"), name);
10574
+ const config = parseJsonc(readFileSync24(join30(cwd, name), "utf8"), name);
10163
10575
  if (config.compilerOptions?.jsx !== void 0) return 'its tsconfig sets "jsx"';
10164
10576
  const extended = Array.isArray(config.extends) ? config.extends : config.extends === void 0 ? [] : [config.extends];
10165
10577
  if (extended.some((entry) => entry === REACT_TYPESCRIPT_PRESET)) {
@@ -10221,8 +10633,8 @@ function resolveFlavour(opts) {
10221
10633
  }
10222
10634
  return detection.preset;
10223
10635
  }
10224
- function previewPlan(opts, plan3) {
10225
- const changed = preparePlan(opts.cwd, plan3).filter((file) => file.before !== file.after);
10636
+ function previewPlan(opts, plan3, prepared) {
10637
+ const changed = prepared.filter((file) => file.before !== file.after);
10226
10638
  if (opts.json) {
10227
10639
  opts.record?.({
10228
10640
  role: opts.target,
@@ -10260,8 +10672,10 @@ async function dispatch(opts) {
10260
10672
  }
10261
10673
  const detected = resolveFlavour(opts);
10262
10674
  const resolutionPreset = opts.preset ?? declaredPresetFor(opts.target, opts.cwd) ?? detected;
10263
- if (resolutionPreset === "tools") {
10264
- const message = `tools modules are not adopted one at a time: they sometimes hold configuration shared with the workspace root, so they move WITH the root, as one reviewed change owned by the tooling team rather than by the module's team.`;
10675
+ const declaredTools = opts.preset === void 0 ? availableTargets().find((target) => declaredPresetFor(target, opts.cwd) === "tools") : void 0;
10676
+ if (resolutionPreset === "tools" || declaredTools !== void 0) {
10677
+ const declaredBy = declaredTools === void 0 || declaredTools === opts.target ? "" : `this module's ${declaredTools} config declares the tools preset, and `;
10678
+ const message = `${declaredBy}tools modules are not adopted one at a time: they sometimes hold configuration shared with the workspace root, so they move WITH the root, as one reviewed change owned by the tooling team rather than by the module's team.`;
10265
10679
  process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${message}`)}
10266
10680
  `);
10267
10681
  opts.record?.({ role: opts.target, status: "deferred", reason: message });
@@ -10304,7 +10718,14 @@ async function dispatch(opts) {
10304
10718
  return 0;
10305
10719
  }
10306
10720
  if (opts.dryRun) {
10307
- return previewPlan(opts, plan3);
10721
+ const prepared = preparePlan(opts.cwd, plan3);
10722
+ opts.planned?.({
10723
+ target: opts.target,
10724
+ adapter,
10725
+ preset,
10726
+ files: prepared.filter((file) => file.before !== file.after)
10727
+ });
10728
+ return previewPlan(opts, plan3, prepared);
10308
10729
  }
10309
10730
  if (adapter.beforeInit) {
10310
10731
  await adapter.beforeInit({
@@ -10357,6 +10778,7 @@ export {
10357
10778
  resolve,
10358
10779
  buildConfigFile,
10359
10780
  palette,
10781
+ presetNotInstalledYet,
10360
10782
  registerAdapters,
10361
10783
  correctingSignal,
10362
10784
  describeFramework,