@hublo/sentinel 1.5.1-alpha.0 → 1.5.1-alpha.2

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,
@@ -19,7 +20,9 @@ import {
19
20
  binFromOwnInstall,
20
21
  binSearchPath,
21
22
  canRunTypeAware,
23
+ canonicalRuleName,
22
24
  describeHolds,
25
+ describeRuleKey,
23
26
  extendsWithBaseline,
24
27
  extendsWithPreset,
25
28
  formatPresetFor,
@@ -51,11 +54,12 @@ import {
51
54
  resolveOxfmt,
52
55
  resolveOxlint,
53
56
  rewriteToolchainImports,
57
+ scriptPath,
54
58
  selfCommand,
55
59
  splitToolArgs,
56
60
  walk,
57
61
  writtenFileFormatter
58
- } from "./chunk-I2WDZNPR.js";
62
+ } from "./chunk-P7NYSMLS.js";
59
63
  import {
60
64
  DEFAULT_MAX_DIAGNOSTICS,
61
65
  WORKSPACE_ROOT_MARKER,
@@ -77,6 +81,9 @@ import {
77
81
  isPlainObject,
78
82
  overridableSettings
79
83
  } from "./chunk-WBDBRGLV.js";
84
+ import {
85
+ globToRegExp
86
+ } from "./chunk-O7REVMOC.js";
80
87
  import {
81
88
  parseJsonc
82
89
  } from "./chunk-QXFCZON7.js";
@@ -153,7 +160,7 @@ function resolve(target, preset, runner) {
153
160
  // src/shared/color.ts
154
161
  import { styleText } from "node:util";
155
162
  function palette(stream) {
156
- const paint = (format, text) => styleText(format, text, { stream });
163
+ const paint = (format2, text) => styleText(format2, text, { stream });
157
164
  return {
158
165
  ok: (text) => paint(["green", "bold"], text),
159
166
  fail: (text) => paint(["red", "bold"], text),
@@ -581,6 +588,19 @@ function composeToolScript(existing, options) {
581
588
  function keepsOtherCommands(script, sentinelCommand) {
582
589
  return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
583
590
  }
591
+ function leadingCommands(script) {
592
+ if (script === void 0) return void 0;
593
+ const parts = script.split(SEGMENT_SEPARATOR);
594
+ const leading = [];
595
+ for (let index = 0; index < parts.length; index += 2) {
596
+ const segment = (parts[index] ?? "").trim();
597
+ if (invokes(segment, "sentinel") || segment.includes("bin/sentinel.js")) break;
598
+ if (index + 1 >= parts.length) return void 0;
599
+ if ((parts[index + 1] ?? "").trim() !== "&&") return void 0;
600
+ leading.push(segment);
601
+ }
602
+ return leading.length > 0 ? leading.join(" && ") : void 0;
603
+ }
584
604
 
585
605
  // src/roles/build/dev-script.ts
586
606
  var CHAIN = " -- ";
@@ -819,8 +839,10 @@ function planReactApp(context2, configFile) {
819
839
  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
840
  };
821
841
  }
842
+ const rewritten = [];
822
843
  if (rewrite.kind === "rewritten") {
823
844
  operations.push({ kind: "write", path: configFile, contents: rewrite.source });
845
+ rewritten.push(configFile);
824
846
  }
825
847
  for (const name of COMPANION_CONFIG_FILES) {
826
848
  const source = readConfigSource(context2.cwd, name);
@@ -828,6 +850,7 @@ function planReactApp(context2, configFile) {
828
850
  const companion = rewriteBuildImports(source, name);
829
851
  if (companion.kind === "rewritten") {
830
852
  operations.push({ kind: "write", path: name, contents: companion.source });
853
+ rewritten.push(name);
831
854
  notes.push(`${name} now imports ${companion.moved.join(", ")} from sentinel too.`);
832
855
  } else if (companion.kind === "blocked") {
833
856
  notes.push(
@@ -861,10 +884,12 @@ function planReactApp(context2, configFile) {
861
884
  `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
885
  );
863
886
  }
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
- );
887
+ if (rewritten.length > 0) {
888
+ const files = rewritten.join(", ");
889
+ notes.push(
890
+ 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.`
891
+ );
892
+ }
868
893
  notes.push(
869
894
  `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
895
  );
@@ -1266,8 +1291,9 @@ function registerBuild() {
1266
1291
 
1267
1292
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1268
1293
  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";
1294
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
1295
+ import { join as join11 } from "node:path";
1296
+ import { isDeepStrictEqual } from "node:util";
1271
1297
 
1272
1298
  // src/core/config/has-source.ts
1273
1299
  import { readdirSync } from "node:fs";
@@ -1361,6 +1387,160 @@ function scopeFlags(paths, options = []) {
1361
1387
  return [TOLERATE_EMPTY_FLAG];
1362
1388
  }
1363
1389
 
1390
+ // src/roles/format/deliberate-import-order.ts
1391
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
1392
+ import { join as join8 } from "node:path";
1393
+ import { parseSync } from "oxc-parser";
1394
+ import { format } from "oxfmt";
1395
+
1396
+ // src/roles/format/presets/deliberate-import-order.json
1397
+ var deliberate_import_order_default = {
1398
+ rules: [
1399
+ "import/order",
1400
+ "import/first",
1401
+ "import-x/order",
1402
+ "import-x/first",
1403
+ "simple-import-sort/imports",
1404
+ "simple-import-sort/exports"
1405
+ ],
1406
+ modules: [
1407
+ "dd-trace",
1408
+ "dd-trace/**",
1409
+ "reflect-metadata",
1410
+ "source-map-support/register",
1411
+ "@opentelemetry/sdk-**",
1412
+ "@opentelemetry/auto-instrumentations-**",
1413
+ "core-js",
1414
+ "core-js/**",
1415
+ "regenerator-runtime",
1416
+ "regenerator-runtime/**"
1417
+ ]
1418
+ };
1419
+
1420
+ // src/roles/format/deliberate-import-order.ts
1421
+ var PIN_COMMENT = "// oxfmt-ignore";
1422
+ var CODE_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx"];
1423
+ var ORDER_RULES = deliberate_import_order_default.rules;
1424
+ var LOAD_ORDER_MODULES = deliberate_import_order_default.modules.map((glob) => globToRegExp(glob));
1425
+ var MODULE_NAMES = [
1426
+ ...new Set(deliberate_import_order_default.modules.map((glob) => glob.split("*")[0] ?? glob))
1427
+ ];
1428
+ function disabledRules(comment, directive) {
1429
+ const match = new RegExp(String.raw`^\s*${directive}(?:\s+(.*?))?\s*(?:--.*)?$`).exec(comment);
1430
+ if (match === null) return void 0;
1431
+ return (match[1] ?? "").split(/[\s,]+/).filter(Boolean);
1432
+ }
1433
+ function namesOrderRule(rules) {
1434
+ return rules !== void 0 && rules.some((rule) => ORDER_RULES.includes(rule));
1435
+ }
1436
+ function lineOf(starts, offset) {
1437
+ let low = 0;
1438
+ let high = starts.length - 1;
1439
+ while (low < high) {
1440
+ const middle = Math.ceil((low + high) / 2);
1441
+ if ((starts[middle] ?? 0) <= offset) low = middle;
1442
+ else high = middle - 1;
1443
+ }
1444
+ return low;
1445
+ }
1446
+ function readImports(fileName, text) {
1447
+ const { program, comments, errors } = parseSync(fileName, text, { sourceType: "module" });
1448
+ if (errors.length > 0) return [];
1449
+ const starts = [0];
1450
+ for (let index = 0; index < text.length; index++) if (text[index] === "\n") starts.push(index + 1);
1451
+ const nextLine = /* @__PURE__ */ new Set();
1452
+ const sameLine = /* @__PURE__ */ new Set();
1453
+ const regions = [];
1454
+ let regionStart;
1455
+ for (const comment of comments) {
1456
+ const line = lineOf(starts, comment.start);
1457
+ if (comment.type === "Line") {
1458
+ if (namesOrderRule(disabledRules(comment.value, "eslint-disable-next-line"))) {
1459
+ nextLine.add(lineOf(starts, comment.end) + 1);
1460
+ }
1461
+ if (namesOrderRule(disabledRules(comment.value, "eslint-disable-line"))) sameLine.add(line);
1462
+ continue;
1463
+ }
1464
+ if (regionStart === void 0 && namesOrderRule(disabledRules(comment.value, "eslint-disable"))) {
1465
+ regionStart = comment.end;
1466
+ continue;
1467
+ }
1468
+ const enabled = disabledRules(comment.value, "eslint-enable");
1469
+ if (regionStart !== void 0 && enabled !== void 0) {
1470
+ if (enabled.length === 0 || namesOrderRule(enabled)) {
1471
+ regions.push([regionStart, comment.start]);
1472
+ regionStart = void 0;
1473
+ }
1474
+ }
1475
+ }
1476
+ if (regionStart !== void 0) regions.push([regionStart, text.length]);
1477
+ const imports = [];
1478
+ for (const node of program.body) {
1479
+ if (node.type !== "ImportDeclaration") continue;
1480
+ const line = lineOf(starts, node.start);
1481
+ const source = node.source.value;
1482
+ const typeOnly = node.importKind === "type";
1483
+ 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));
1484
+ imports.push({ line, source, signalled });
1485
+ }
1486
+ return imports;
1487
+ }
1488
+ function wouldMove(before, after) {
1489
+ const order = (list) => list.map((entry) => entry.source);
1490
+ const was = order(before);
1491
+ const now = order(after);
1492
+ return before.some((entry, at) => {
1493
+ if (!entry.signalled) return false;
1494
+ const earlier = new Set(was.slice(0, at));
1495
+ const index = now.indexOf(entry.source);
1496
+ const nowEarlier = new Set(now.slice(0, index));
1497
+ return earlier.size !== nowEarlier.size || [...earlier].some((name) => !nowEarlier.has(name));
1498
+ });
1499
+ }
1500
+ function pin(text, imports) {
1501
+ const lines = text.split("\n");
1502
+ const insertAt = [];
1503
+ for (const { line, signalled } of imports) {
1504
+ if (!signalled) continue;
1505
+ let top = line;
1506
+ while (top > 0 && /^\s*\/\//.test(lines[top - 1] ?? "")) top -= 1;
1507
+ if (lines.slice(top, line).some((above) => above.trim() === PIN_COMMENT)) continue;
1508
+ insertAt.push(top);
1509
+ }
1510
+ for (const at of [...new Set(insertAt)].sort((a, b) => b - a)) {
1511
+ const indent = /^\s*/.exec(lines[at] ?? "")?.[0] ?? "";
1512
+ lines.splice(at, 0, `${indent}${PIN_COMMENT}`);
1513
+ }
1514
+ return lines.join("\n");
1515
+ }
1516
+ function formatOptions(config) {
1517
+ const {
1518
+ $sentinel: _provenance,
1519
+ ignorePatterns: _ignored,
1520
+ overrides: _overrides,
1521
+ ...options
1522
+ } = config;
1523
+ return options;
1524
+ }
1525
+ async function pinDeliberateImportOrder(cwd, options) {
1526
+ if (!options.sortImports) return [];
1527
+ const pinned = [];
1528
+ for (const file of listSourceFiles(cwd, CODE_EXTENSIONS).sort()) {
1529
+ const path2 = join8(cwd, file);
1530
+ const text = readFileSync6(path2, "utf8");
1531
+ if (!text.includes("eslint-disable") && !MODULE_NAMES.some((name) => text.includes(name)))
1532
+ continue;
1533
+ const before = readImports(file, text);
1534
+ if (!before.some((entry) => entry.signalled)) continue;
1535
+ const sorted = await format(file, text, options);
1536
+ if (sorted.errors.length > 0) continue;
1537
+ if (!wouldMove(before, readImports(file, sorted.code))) continue;
1538
+ writeFileSync2(path2, pin(text, before));
1539
+ pinned.push(file);
1540
+ }
1541
+ return pinned;
1542
+ }
1543
+
1364
1544
  // src/roles/format/format-script.ts
1365
1545
  var SENTINEL_FORMAT_COMMAND = "sentinel --run --format";
1366
1546
  function isPrettierSegment(segment) {
@@ -1389,8 +1569,8 @@ function writesWhenRewritten(name, command) {
1389
1569
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
1390
1570
 
1391
1571
  // 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";
1572
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
1573
+ import { join as join9 } from "node:path";
1394
1574
  var ROOT_IGNORE_FILE = ".prettierignore";
1395
1575
  function isPattern(line) {
1396
1576
  const trimmed = line.trim();
@@ -1406,11 +1586,11 @@ function toModulePattern(pattern) {
1406
1586
  }
1407
1587
  function inheritedIgnorePatterns(workspaceRoot) {
1408
1588
  if (!workspaceRoot) return [];
1409
- const path2 = join8(workspaceRoot, ROOT_IGNORE_FILE);
1589
+ const path2 = join9(workspaceRoot, ROOT_IGNORE_FILE);
1410
1590
  if (!existsSync6(path2)) return [];
1411
1591
  let contents;
1412
1592
  try {
1413
- contents = readFileSync6(path2, "utf8");
1593
+ contents = readFileSync7(path2, "utf8");
1414
1594
  } catch {
1415
1595
  return [];
1416
1596
  }
@@ -1455,8 +1635,8 @@ function needsSvelteCompiler(output) {
1455
1635
  }
1456
1636
 
1457
1637
  // 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";
1638
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
1639
+ import { join as join10 } from "node:path";
1460
1640
  var UNSUPPORTED = /* @__PURE__ */ new Set(["plugins", "parser", "filepath", "rangeStart", "rangeEnd"]);
1461
1641
  function toOxfmtOverrides(value) {
1462
1642
  if (!Array.isArray(value)) return { overrides: [], unresolved: [] };
@@ -1481,14 +1661,14 @@ function toOxfmtOverrides(value) {
1481
1661
  return { overrides, unresolved };
1482
1662
  }
1483
1663
  function readPrettierSettings(cwd) {
1484
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync7(join9(cwd, name)));
1664
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync7(join10(cwd, name)));
1485
1665
  if (!file) return { options: {}, unresolved: [] };
1486
1666
  if (/\.(js|cjs|mjs)$/.test(file)) {
1487
1667
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
1488
1668
  }
1489
1669
  let parsed;
1490
1670
  try {
1491
- parsed = parseJsonc(readFileSync7(join9(cwd, file), "utf8"), file);
1671
+ parsed = parseJsonc(readFileSync8(join10(cwd, file), "utf8"), file);
1492
1672
  } catch {
1493
1673
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
1494
1674
  }
@@ -1570,6 +1750,11 @@ var OxfmtAdapter = class extends BaseAdapter {
1570
1750
  const existing = readFormatAdoption(context2.cwd);
1571
1751
  const prettier = readPrettierSettings(context2.cwd);
1572
1752
  const local = existing.adopted ? { ...existing.local } : { ...prettier.options };
1753
+ const recorded = existing.preset === null ? void 0 : formatPresetFor(existing.preset);
1754
+ const ownedWhenSet = existing.adopted ? OWNED_WHEN_SET_KEYS.filter(
1755
+ (key) => !(key in local) && key in existing.effective && !isDeepStrictEqual(existing.effective[key], preset[key]) && !isDeepStrictEqual(existing.effective[key], recorded?.[key])
1756
+ ) : [];
1757
+ for (const key of ownedWhenSet) local[key] = existing.effective[key];
1573
1758
  for (const [key, value] of Object.entries(local)) {
1574
1759
  if (ALWAYS_LOCAL_KEYS.includes(key)) continue;
1575
1760
  if (key in preset && JSON.stringify(preset[key]) === JSON.stringify(value)) delete local[key];
@@ -1581,17 +1766,19 @@ var OxfmtAdapter = class extends BaseAdapter {
1581
1766
  .../* @__PURE__ */ new Set([...declaredIgnores, ...inherited, ...FORMAT_DEFAULT_IGNORES])
1582
1767
  ].sort();
1583
1768
  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 },
1769
+ const configStamped = (version2) => ({
1770
+ [PROVENANCE_KEY]: { preset: presetName, version: version2, local: localKeys },
1588
1771
  ...preset,
1589
1772
  ...Object.fromEntries(
1590
1773
  [...localKeys, ...ALWAYS_LOCAL_KEYS].flatMap(
1591
1774
  (key) => key in local ? [[key, local[key]]] : []
1592
1775
  )
1593
1776
  )
1594
- };
1777
+ });
1778
+ const previous = existing.adopted ? existing.presetVersion : null;
1779
+ const unchanged = previous !== null && isDeepStrictEqual(configStamped(previous), existing.effective);
1780
+ const version = unchanged ? previous : own.version;
1781
+ const config = configStamped(version);
1595
1782
  const operations = [
1596
1783
  // Written pre-formatted (2-space, trailing newline): oxfmt formats JSON too, and until
1597
1784
  // the format pass runs this file is the one config nothing has formatted yet.
@@ -1599,7 +1786,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1599
1786
  manifestOperation(context2.cwd, this.formatScripts(context2.cwd))
1600
1787
  ];
1601
1788
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1602
- (name) => existsSync8(join10(context2.cwd, name))
1789
+ (name) => existsSync8(join11(context2.cwd, name))
1603
1790
  );
1604
1791
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1605
1792
  const removableDeps = this.modulePrettierDependencies(context2.cwd);
@@ -1608,8 +1795,13 @@ var OxfmtAdapter = class extends BaseAdapter {
1608
1795
  }
1609
1796
  operations.push(...nxTargetOperations({ cwd: context2.cwd, targets: formatTargets() }));
1610
1797
  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`
1798
+ `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
1799
  ];
1800
+ if (ownedWhenSet.length > 0) {
1801
+ notes.push(
1802
+ `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`
1803
+ );
1804
+ }
1613
1805
  if (localKeys.length > 0) {
1614
1806
  notes.push(
1615
1807
  `kept this module's own formatting (${localKeys.join(", ")})${existing.adopted ? "" : ` from ${prettier.file}`}: the preset is the repo's, these are yours`
@@ -1667,13 +1859,29 @@ var OxfmtAdapter = class extends BaseAdapter {
1667
1859
  async rehearseAfterInit(ctx, origin) {
1668
1860
  const oxfmt = resolveOxfmt(origin);
1669
1861
  if (!oxfmt) return "oxfmt is not installed in this module yet";
1862
+ await this.keepDeliberateImportOrder(ctx.cwd);
1670
1863
  const pass = spawnSync3(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1671
1864
  if (pass.error) return pass.error.message;
1672
1865
  return pass.status === 0 ? void 0 : oxfmtSaid(`${pass.stdout ?? ""}${pass.stderr ?? ""}`);
1673
1866
  }
1867
+ /**
1868
+ * Pin, before the formatter sorts, the imports whose order is deliberate. See
1869
+ * `deliberate-import-order.ts`. Returns the line to print, or undefined when nothing was pinned.
1870
+ */
1871
+ async keepDeliberateImportOrder(cwd) {
1872
+ const pinned = await pinDeliberateImportOrder(
1873
+ cwd,
1874
+ formatOptions(readFormatAdoption(cwd).effective)
1875
+ );
1876
+ if (pinned.length === 0) return void 0;
1877
+ 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`;
1878
+ }
1674
1879
  async afterInit(ctx) {
1675
1880
  const oxfmt = resolveOxfmt(ctx.cwd);
1676
1881
  if (!oxfmt) return;
1882
+ const kept = await this.keepDeliberateImportOrder(ctx.cwd);
1883
+ if (kept !== void 0) process.stderr.write(` ${palette(process.stderr).warn(kept)}
1884
+ `);
1677
1885
  const pass = spawnSync3(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1678
1886
  process.stderr.write(` ${palette(process.stderr).dim(FORMATTER_DIFFERENCES)}
1679
1887
  `);
@@ -1855,7 +2063,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1855
2063
  modulePrettierDependencies(cwd) {
1856
2064
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
1857
2065
  try {
1858
- const manifest = JSON.parse(readFileSync8(join10(cwd, "package.json"), "utf8"));
2066
+ const manifest = JSON.parse(readFileSync9(join11(cwd, "package.json"), "utf8"));
1859
2067
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
1860
2068
  } catch {
1861
2069
  return [];
@@ -1937,8 +2145,8 @@ function registerFormat() {
1937
2145
 
1938
2146
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1939
2147
  import { spawnSync as spawnSync4 } from "node:child_process";
1940
- import { existsSync as existsSync12, readFileSync as readFileSync13, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
1941
- import { join as join16 } from "node:path";
2148
+ import { existsSync as existsSync12, readFileSync as readFileSync14, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
2149
+ import { join as join17 } from "node:path";
1942
2150
 
1943
2151
  // src/core/config/deferred-rules.ts
1944
2152
  function deferredRuleNames(rules) {
@@ -1947,7 +2155,7 @@ function deferredRuleNames(rules) {
1947
2155
 
1948
2156
  // src/core/fix-report.ts
1949
2157
  import { statSync as statSync2 } from "node:fs";
1950
- import { join as join11 } from "node:path";
2158
+ import { join as join12 } from "node:path";
1951
2159
  var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
1952
2160
  function readFailure(output) {
1953
2161
  const lines = output.split("\n").map((line2) => line2.trim());
@@ -1967,7 +2175,7 @@ function fingerprint(path2) {
1967
2175
  function snapshotFiles(cwd, extensions) {
1968
2176
  const snapshot = /* @__PURE__ */ new Map();
1969
2177
  for (const file of listSourceFiles(cwd, extensions)) {
1970
- const mark = fingerprint(join11(cwd, file));
2178
+ const mark = fingerprint(join12(cwd, file));
1971
2179
  if (mark !== void 0) snapshot.set(file, mark);
1972
2180
  }
1973
2181
  return snapshot;
@@ -1975,7 +2183,7 @@ function snapshotFiles(cwd, extensions) {
1975
2183
  function changedSince(cwd, before, extensions) {
1976
2184
  const changed = [];
1977
2185
  for (const file of listSourceFiles(cwd, extensions)) {
1978
- const now = fingerprint(join11(cwd, file));
2186
+ const now = fingerprint(join12(cwd, file));
1979
2187
  if (now === void 0) continue;
1980
2188
  if (before.get(file) !== now) changed.push(file);
1981
2189
  }
@@ -4381,6 +4589,19 @@ function validate(preset, rules) {
4381
4589
  }
4382
4590
  }
4383
4591
  }
4592
+ function rejectTwoSpellings(label, rules) {
4593
+ const seen = /* @__PURE__ */ new Map();
4594
+ for (const rule of Object.keys(rules)) {
4595
+ const canonical = canonicalRuleName(rule);
4596
+ const other = seen.get(canonical);
4597
+ if (other !== void 0) {
4598
+ throw new Error(
4599
+ `${label}: "${other}" and "${rule}" are the same rule to oxlint (${canonical}). Configure it once, under one name: oxlint keeps both and picks one by hash order.`
4600
+ );
4601
+ }
4602
+ seen.set(canonical, rule);
4603
+ }
4604
+ }
4384
4605
  function layered(preset) {
4385
4606
  const base = sharedRules();
4386
4607
  const layer = presetRules(preset);
@@ -4390,6 +4611,7 @@ function layered(preset) {
4390
4611
  }
4391
4612
  function toRuleMap(label, rules) {
4392
4613
  validate(label, rules);
4614
+ rejectTwoSpellings(label, rules);
4393
4615
  const entries = Object.entries(rules).map(([rule, value]) => {
4394
4616
  if (!isDecision(value)) return [rule, value];
4395
4617
  const options = value.options ?? [];
@@ -4421,14 +4643,14 @@ function downgradedRulesFor(preset) {
4421
4643
  }
4422
4644
 
4423
4645
  // src/roles/lint/extra-layers.ts
4424
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
4425
- import { isAbsolute, join as join12, resolve as resolve2 } from "node:path";
4646
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
4647
+ import { isAbsolute, join as join13, resolve as resolve2 } from "node:path";
4426
4648
  function ruleCount(cwd, specifier) {
4427
4649
  const path2 = isAbsolute(specifier) ? specifier : resolve2(cwd, specifier);
4428
4650
  if (!existsSync9(path2)) return void 0;
4429
4651
  try {
4430
4652
  const parsed = parseJsonc(
4431
- readFileSync9(path2, "utf8"),
4653
+ readFileSync10(path2, "utf8"),
4432
4654
  specifier
4433
4655
  );
4434
4656
  return Object.keys(parsed.rules ?? {}).length;
@@ -4448,7 +4670,7 @@ function extraLayers(cwd, extendsList) {
4448
4670
  function committedExtendsList(cwd, configFile) {
4449
4671
  try {
4450
4672
  const parsed = parseJsonc(
4451
- readFileSync9(join12(cwd, configFile), "utf8"),
4673
+ readFileSync10(join13(cwd, configFile), "utf8"),
4452
4674
  configFile
4453
4675
  );
4454
4676
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4457,6 +4679,110 @@ function committedExtendsList(cwd, configFile) {
4457
4679
  }
4458
4680
  }
4459
4681
 
4682
+ // src/roles/lint/module-own-config.ts
4683
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4684
+ function pluginKey(entry) {
4685
+ if (typeof entry === "string") return entry;
4686
+ if (isRecord(entry) && typeof entry.name === "string") return entry.name;
4687
+ if (isRecord(entry) && typeof entry.specifier === "string") return entry.specifier;
4688
+ return void 0;
4689
+ }
4690
+ function governedRuleNames(preset) {
4691
+ return /* @__PURE__ */ new Set([
4692
+ ...Object.keys(preset.rules),
4693
+ ...(preset.overrides ?? []).flatMap((override) => Object.keys(override.rules))
4694
+ ]);
4695
+ }
4696
+ function governedRules(preset) {
4697
+ const governed = /* @__PURE__ */ new Map();
4698
+ for (const name of governedRuleNames(preset)) {
4699
+ const canonical = canonicalRuleName(name);
4700
+ if (!governed.has(canonical)) governed.set(canonical, name);
4701
+ }
4702
+ return governed;
4703
+ }
4704
+ function moduleOwnBlocks(committed, preset) {
4705
+ const blocks = {};
4706
+ const kept = [];
4707
+ const dropped = [];
4708
+ if (Array.isArray(committed.plugins)) {
4709
+ const own = committed.plugins.filter(
4710
+ (name) => typeof name === "string" && !preset.plugins.includes(name)
4711
+ );
4712
+ if (own.length > 0) {
4713
+ blocks.plugins = own;
4714
+ kept.push(
4715
+ `kept this module's own plugins (${own.join(", ")}): the preset does not enable them`
4716
+ );
4717
+ }
4718
+ }
4719
+ if (Array.isArray(committed.jsPlugins)) {
4720
+ const presetKeys = new Set(preset.jsPlugins.map((entry) => pluginKey(entry)));
4721
+ const own = [];
4722
+ for (const entry of committed.jsPlugins) {
4723
+ const key = pluginKey(entry);
4724
+ if (key !== void 0 && presetKeys.has(key)) dropped.push(`jsPlugins ${key}`);
4725
+ else own.push(entry);
4726
+ }
4727
+ if (own.length > 0) {
4728
+ blocks.jsPlugins = own;
4729
+ kept.push(
4730
+ `kept ${own.length} jsPlugin(s) this module added (${own.map((entry) => pluginKey(entry) ?? "?").join(", ")})`
4731
+ );
4732
+ }
4733
+ }
4734
+ if (isRecord(committed.settings)) {
4735
+ const presetSettings = preset.settings ?? {};
4736
+ const own = {};
4737
+ for (const [key, value] of Object.entries(committed.settings)) {
4738
+ if (key in presetSettings) dropped.push(`settings.${key}`);
4739
+ else own[key] = value;
4740
+ }
4741
+ if (Object.keys(own).length > 0) {
4742
+ blocks.settings = own;
4743
+ kept.push(`kept this module's own settings (${Object.keys(own).join(", ")})`);
4744
+ }
4745
+ }
4746
+ if (Array.isArray(committed.overrides)) {
4747
+ const governed = governedRules(preset);
4748
+ const byFiles = /* @__PURE__ */ new Map();
4749
+ for (const block of committed.overrides) {
4750
+ if (!isRecord(block) || !Array.isArray(block.files)) continue;
4751
+ const files = JSON.stringify([...block.files].map(String).sort());
4752
+ const rules = {};
4753
+ for (const [rule, value] of Object.entries(isRecord(block.rules) ? block.rules : {})) {
4754
+ const presetSpelling = governed.get(canonicalRuleName(rule));
4755
+ if (presetSpelling === void 0) rules[rule] = value;
4756
+ else
4757
+ dropped.push(
4758
+ `overrides[${block.files.join(", ")}] ${describeRuleKey(rule, presetSpelling)}`
4759
+ );
4760
+ }
4761
+ const { rules: _rules, ...rest } = block;
4762
+ const merged = byFiles.get(files);
4763
+ if (merged === void 0) {
4764
+ byFiles.set(files, { ...rest, ...Object.keys(rules).length > 0 ? { rules } : {} });
4765
+ continue;
4766
+ }
4767
+ const { files: _sameFiles, ...others } = rest;
4768
+ const mergedRules = { ...isRecord(merged.rules) ? merged.rules : {}, ...rules };
4769
+ Object.assign(
4770
+ merged,
4771
+ others,
4772
+ Object.keys(mergedRules).length > 0 ? { rules: mergedRules } : {}
4773
+ );
4774
+ }
4775
+ const own = [...byFiles.values()].filter((block) => Object.keys(block).length > 1);
4776
+ if (own.length > 0) {
4777
+ blocks.overrides = own;
4778
+ kept.push(
4779
+ `kept ${own.length} override block(s) this module set (${own.map((block) => block.files.join(", ")).join("; ")})`
4780
+ );
4781
+ }
4782
+ }
4783
+ return { blocks, kept, dropped };
4784
+ }
4785
+
4460
4786
  // src/roles/lint/parse-diagnostics.ts
4461
4787
  function ruleId(code) {
4462
4788
  const inner = /\(([^)]+)\)/.exec(code);
@@ -4506,14 +4832,34 @@ function errorCountsByConfigRule(stdout) {
4506
4832
  return counts;
4507
4833
  }
4508
4834
 
4835
+ // src/roles/lint/presets.ts
4836
+ var hasRuleData = (name) => GENERATED_LINT_PRESETS.includes(name);
4837
+ function lintPresetFor(preset) {
4838
+ const file = presetFile(preset);
4839
+ const resolved = hasRuleData(preset) ? preset : "node";
4840
+ return {
4841
+ plugins: file.plugins,
4842
+ jsPlugins: file.jsPlugins,
4843
+ ...file.settings === null ? {} : { settings: file.settings },
4844
+ ...file.renames ? { renames: file.renames } : {},
4845
+ ...file.overrides ? {
4846
+ overrides: file.overrides.map((override, index) => ({
4847
+ files: override.files,
4848
+ rules: toRuleMap(`${preset} override ${index}`, override.rules)
4849
+ }))
4850
+ } : {},
4851
+ rules: rulesFor(resolved)
4852
+ };
4853
+ }
4854
+
4509
4855
  // src/roles/lint/adapters/oxlint/plan.ts
4510
- import { existsSync as existsSync11, readFileSync as readFileSync12 } from "node:fs";
4511
- import { join as join15 } from "node:path";
4856
+ import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
4857
+ import { join as join16 } from "node:path";
4512
4858
 
4513
4859
  // src/roles/lint/eslint-ignores.ts
4514
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
4515
- import { join as join13 } from "node:path";
4516
- import { parseSync } from "oxc-parser";
4860
+ import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
4861
+ import { join as join14 } from "node:path";
4862
+ import { parseSync as parseSync2 } from "oxc-parser";
4517
4863
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
4518
4864
  function toOxlintPattern(pattern) {
4519
4865
  return pattern.replace(/^\.\//, "");
@@ -4524,17 +4870,17 @@ function readRootEslintIgnores(root) {
4524
4870
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4525
4871
  }
4526
4872
  function readEslintIgnores(cwd) {
4527
- const config = ESLINT_CONFIG_FILES.map((name) => join13(cwd, name)).find((path2) => existsSync10(path2));
4873
+ const config = ESLINT_CONFIG_FILES.map((name) => join14(cwd, name)).find((path2) => existsSync10(path2));
4528
4874
  if (!config) return { patterns: [], unresolved: [] };
4529
4875
  let source;
4530
4876
  try {
4531
- source = readFileSync10(config, "utf8");
4877
+ source = readFileSync11(config, "utf8");
4532
4878
  } catch {
4533
4879
  return { patterns: [], unresolved: [] };
4534
4880
  }
4535
4881
  const patterns = /* @__PURE__ */ new Set();
4536
4882
  const unresolved = new Set([...source.matchAll(OPAQUE_SOURCE)].map((match) => match[0]));
4537
- const { program, errors } = parseSync(config, source, { sourceType: "module" });
4883
+ const { program, errors } = parseSync2(config, source, { sourceType: "module" });
4538
4884
  if (errors.length > 0) {
4539
4885
  return { patterns: [], unresolved: [`${config} could not be parsed`] };
4540
4886
  }
@@ -4598,29 +4944,9 @@ function keepsOtherCommands3(script) {
4598
4944
  return keepsOtherCommands(script, SENTINEL_LINT_COMMAND);
4599
4945
  }
4600
4946
 
4601
- // src/roles/lint/presets.ts
4602
- var hasRuleData = (name) => GENERATED_LINT_PRESETS.includes(name);
4603
- function lintPresetFor(preset) {
4604
- const file = presetFile(preset);
4605
- const resolved = hasRuleData(preset) ? preset : "node";
4606
- return {
4607
- plugins: file.plugins,
4608
- jsPlugins: file.jsPlugins,
4609
- ...file.settings === null ? {} : { settings: file.settings },
4610
- ...file.renames ? { renames: file.renames } : {},
4611
- ...file.overrides ? {
4612
- overrides: file.overrides.map((override, index) => ({
4613
- files: override.files,
4614
- rules: toRuleMap(`${preset} override ${index}`, override.rules)
4615
- }))
4616
- } : {},
4617
- rules: rulesFor(resolved)
4618
- };
4619
- }
4620
-
4621
4947
  // src/roles/lint/rename-suppressions.ts
4622
- import { readdirSync as readdirSync2, readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
4623
- import { join as join14, relative as relative3 } from "node:path";
4948
+ import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync as statSync3 } from "node:fs";
4949
+ import { join as join15, relative as relative3 } from "node:path";
4624
4950
  var SOURCE_EXTENSIONS = [
4625
4951
  ".ts",
4626
4952
  ".tsx",
@@ -4667,7 +4993,7 @@ function* sourceFiles(dir) {
4667
4993
  return;
4668
4994
  }
4669
4995
  for (const entry of entries) {
4670
- const full = join14(dir, entry);
4996
+ const full = join15(dir, entry);
4671
4997
  let isDirectory;
4672
4998
  try {
4673
4999
  isDirectory = statSync3(full).isDirectory();
@@ -4687,7 +5013,7 @@ function findSuppressionRenames(cwd, renames) {
4687
5013
  for (const file of sourceFiles(cwd)) {
4688
5014
  let content;
4689
5015
  try {
4690
- content = readFileSync11(file, "utf8");
5016
+ content = readFileSync12(file, "utf8");
4691
5017
  } catch {
4692
5018
  continue;
4693
5019
  }
@@ -4807,13 +5133,18 @@ function plan2(context2) {
4807
5133
  ...rootIgnores.patterns
4808
5134
  ].filter((pattern) => !defaults2.has(bare(pattern)));
4809
5135
  const carried = [.../* @__PURE__ */ new Set([...DEFAULT_IGNORE_PATTERNS, ...moduleOwn])];
5136
+ const committed = committedConfig(context2.cwd);
4810
5137
  const { kept: teamRules, governed: droppedRules } = moduleOwnRules(
4811
- committedRules(context2.cwd),
5138
+ isRecord2(committed.rules) ? committed.rules : {},
4812
5139
  lintPresetFor(variant)
4813
5140
  );
5141
+ const teamBlocks = moduleOwnBlocks(committed, lintPresetFor(variant));
5142
+ const { overrides: teamOverrides, ...teamTopLevel } = teamBlocks.blocks;
4814
5143
  const stub = {
4815
5144
  extends: extendsWithPreset(committedExtends(context2.cwd), variant),
5145
+ ...teamTopLevel,
4816
5146
  ...Object.keys(teamRules).length > 0 ? { rules: teamRules } : {},
5147
+ ...teamOverrides !== void 0 ? { overrides: teamOverrides } : {},
4817
5148
  ...carried.length > 0 ? { ignorePatterns: carried } : {}
4818
5149
  };
4819
5150
  const operations = [
@@ -4828,7 +5159,7 @@ function plan2(context2) {
4828
5159
  keys: removableDeps
4829
5160
  });
4830
5161
  }
4831
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync11(join15(context2.cwd, name)));
5162
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync11(join16(context2.cwd, name)));
4832
5163
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4833
5164
  operations.push(
4834
5165
  ...nxTargetOperations({
@@ -4869,7 +5200,13 @@ function plan2(context2) {
4869
5200
  }
4870
5201
  if (droppedRules.length > 0) {
4871
5202
  notes.push(
4872
- `dropped ${droppedRules.length} rule(s) this module set over the preset (${droppedRules.join(", ")}): the preset governs them`
5203
+ `dropped ${droppedRules.length} rule(s) this module set over the preset (${droppedRules.map((rule) => describeGoverned(rule, variant)).join(", ")}): the preset governs them`
5204
+ );
5205
+ }
5206
+ notes.push(...teamBlocks.kept);
5207
+ if (teamBlocks.dropped.length > 0) {
5208
+ notes.push(
5209
+ `dropped ${teamBlocks.dropped.length} entr(ies) this module set over the preset (${teamBlocks.dropped.join(", ")}): the preset governs them`
4873
5210
  );
4874
5211
  }
4875
5212
  if (rootIgnores.patterns.length > 0) {
@@ -4936,7 +5273,7 @@ function lintScripts(cwd) {
4936
5273
  function committedExtends(cwd) {
4937
5274
  try {
4938
5275
  const parsed = parseJsonc(
4939
- readFileSync12(join15(cwd, LINT_CONFIG_FILE), "utf8"),
5276
+ readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
4940
5277
  LINT_CONFIG_FILE
4941
5278
  );
4942
5279
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4944,35 +5281,35 @@ function committedExtends(cwd) {
4944
5281
  return [];
4945
5282
  }
4946
5283
  }
4947
- function committedRules(cwd) {
5284
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5285
+ function committedConfig(cwd) {
4948
5286
  try {
4949
5287
  const parsed = parseJsonc(
4950
- readFileSync12(join15(cwd, LINT_CONFIG_FILE), "utf8"),
5288
+ readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
4951
5289
  LINT_CONFIG_FILE
4952
5290
  );
4953
- const rules = parsed.rules;
4954
- return typeof rules === "object" && rules !== null && !Array.isArray(rules) ? rules : {};
5291
+ return isRecord2(parsed) ? parsed : {};
4955
5292
  } catch {
4956
5293
  return {};
4957
5294
  }
4958
5295
  }
4959
5296
  function moduleOwnRules(committed, preset) {
4960
- const governedNames = /* @__PURE__ */ new Set([
4961
- ...Object.keys(preset.rules),
4962
- ...(preset.overrides ?? []).flatMap((override) => Object.keys(override.rules))
4963
- ]);
5297
+ const governedNames = governedRules(preset);
4964
5298
  const kept = {};
4965
5299
  const governed = [];
4966
5300
  for (const [rule, value] of Object.entries(committed)) {
4967
- if (governedNames.has(rule)) governed.push(rule);
5301
+ if (governedNames.has(canonicalRuleName(rule))) governed.push(rule);
4968
5302
  else kept[rule] = value;
4969
5303
  }
4970
5304
  return { kept, governed };
4971
5305
  }
5306
+ function describeGoverned(rule, variant) {
5307
+ return describeRuleKey(rule, governedRules(lintPresetFor(variant)).get(canonicalRuleName(rule)));
5308
+ }
4972
5309
  function committedIgnorePatterns(cwd) {
4973
5310
  try {
4974
5311
  const parsed = parseJsonc(
4975
- readFileSync12(join15(cwd, LINT_CONFIG_FILE), "utf8"),
5312
+ readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
4976
5313
  LINT_CONFIG_FILE
4977
5314
  );
4978
5315
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4984,7 +5321,7 @@ function moduleEslintDependencies(cwd) {
4984
5321
  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/");
4985
5322
  let manifest;
4986
5323
  try {
4987
- manifest = JSON.parse(readFileSync12(join15(cwd, "package.json"), "utf8"));
5324
+ manifest = JSON.parse(readFileSync13(join16(cwd, "package.json"), "utf8"));
4988
5325
  } catch {
4989
5326
  return [];
4990
5327
  }
@@ -5098,7 +5435,44 @@ ${result.stderr ?? ""}`);
5098
5435
  const env = { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) };
5099
5436
  process.stderr.write(` ${describeFixOutcome(this.fixPass(ctx, oxlint, env), "lint")}
5100
5437
  `);
5101
- this.writeModuleBaseline(ctx, oxlint, env);
5438
+ if (this.runLeadingCommands(ctx, env)) this.writeModuleBaseline(ctx, oxlint, env);
5439
+ }
5440
+ /**
5441
+ * Run what the module's `lint` script runs before sentinel, so the baseline measures the module
5442
+ * `pnpm run lint` lints. See `leadingCommands`: on hr-management that is `svelte-kit sync`, and
5443
+ * without it the measurement met a tsconfig extending a file that did not exist yet and held
5444
+ * `typescript/tsconfig-error`, a hold a fresh clone would commit.
5445
+ *
5446
+ * Shelled the way a package manager runs a script, with every `node_modules/.bin` up the tree
5447
+ * on PATH. False when the command failed: a baseline measured without it is the wrong module,
5448
+ * and recording that is worse than recording nothing, so the existing baseline is left alone
5449
+ * and the developer is told what to run.
5450
+ */
5451
+ runLeadingCommands(ctx, env) {
5452
+ const leading = leadingCommands(moduleScripts(ctx.cwd)[LINT_SCRIPT_NAME]);
5453
+ if (leading === void 0) return true;
5454
+ const result = spawnSync4(leading, {
5455
+ cwd: ctx.cwd,
5456
+ shell: true,
5457
+ encoding: "utf8",
5458
+ env: { ...env, PATH: scriptPath(ctx.cwd, env.PATH) },
5459
+ maxBuffer: CAPTURE_MAX_BUFFER
5460
+ });
5461
+ if (!result.error && result.status === 0) {
5462
+ process.stderr.write(
5463
+ ` ran \`${leading}\` before measuring, as the lint script runs it before sentinel
5464
+ `
5465
+ );
5466
+ return true;
5467
+ }
5468
+ const said = (result.error?.message ?? `${result.stderr ?? ""}${result.stdout ?? ""}`).trim().split("\n").slice(-1)[0];
5469
+ process.stderr.write(
5470
+ ` ${palette(process.stderr).warn(
5471
+ `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.`
5472
+ )}
5473
+ `
5474
+ );
5475
+ return false;
5102
5476
  }
5103
5477
  /**
5104
5478
  * The fix pass alone, in the copy `--init --dry-run` hands it, without the baseline measurement
@@ -5128,23 +5502,23 @@ ${result.stderr ?? ""}`);
5128
5502
  * build the developer can see, rather than a silent half-adoption they cannot.
5129
5503
  */
5130
5504
  writeModuleBaseline(ctx, oxlint, env) {
5131
- const configPath = join16(ctx.cwd, LINT_CONFIG_FILE);
5132
- const baselinePath = join16(ctx.cwd, LINT_BASELINE_FILE);
5505
+ const configPath = join17(ctx.cwd, LINT_CONFIG_FILE);
5506
+ const baselinePath = join17(ctx.cwd, LINT_BASELINE_FILE);
5133
5507
  if (!existsSync12(configPath)) return;
5134
5508
  let config;
5135
5509
  try {
5136
5510
  config = parseJsonc(
5137
- readFileSync13(configPath, "utf8"),
5511
+ readFileSync14(configPath, "utf8"),
5138
5512
  LINT_CONFIG_FILE
5139
5513
  );
5140
5514
  } catch {
5141
5515
  return;
5142
5516
  }
5143
5517
  const current = Array.isArray(config.extends) ? config.extends : [];
5144
- const measurePath = join16(ctx.cwd, LINT_MEASURE_FILE);
5518
+ const measurePath = join17(ctx.cwd, LINT_MEASURE_FILE);
5145
5519
  let measured;
5146
5520
  try {
5147
- writeFileSync2(
5521
+ writeFileSync3(
5148
5522
  measurePath,
5149
5523
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
5150
5524
  `
@@ -5173,10 +5547,13 @@ ${result.stderr ?? ""}`);
5173
5547
  );
5174
5548
  return;
5175
5549
  }
5176
- const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
5550
+ const holds = holdsFrom(
5551
+ errorCountsByConfigRule(measured.stdout ?? ""),
5552
+ governedRules(lintPresetFor(presetVariant(ctx.preset, ctx.cwd)))
5553
+ );
5177
5554
  const next = extendsWithBaseline(current, holds.length > 0);
5178
5555
  if (holds.length > 0) {
5179
- writeFileSync2(baselinePath, renderBaseline(holds, readOwnPackage().version));
5556
+ writeFileSync3(baselinePath, renderBaseline(holds, readOwnPackage().version));
5180
5557
  process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
5181
5558
  `);
5182
5559
  } else {
@@ -5184,12 +5561,12 @@ ${result.stderr ?? ""}`);
5184
5561
  }
5185
5562
  if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
5186
5563
  config.extends = next;
5187
- writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
5564
+ writeFileSync3(configPath, `${JSON.stringify(config, null, 2)}
5188
5565
  `);
5189
5566
  }
5190
5567
  }
5191
5568
  async run(ctx) {
5192
- if (!existsSync12(join16(ctx.cwd, LINT_CONFIG_FILE))) {
5569
+ if (!existsSync12(join17(ctx.cwd, LINT_CONFIG_FILE))) {
5193
5570
  process.stderr.write(
5194
5571
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
5195
5572
  `
@@ -5329,7 +5706,8 @@ ${result.stderr ?? ""}`);
5329
5706
  * exactly when something is broken.
5330
5707
  *
5331
5708
  * Counted as a SET of rule names, so the baseline re-stating a preset rule at `warn` is
5332
- * the same rule, not a second one.
5709
+ * the same rule, not a second one. The rule oxlint RUNS (`canonicalRuleName`), so a layer
5710
+ * spelling it `no-unused-vars` beside the preset's `typescript/no-unused-vars` is one rule too.
5333
5711
  */
5334
5712
  presetRuleCount(cwd) {
5335
5713
  const target = this.unresolvedPreset(cwd);
@@ -5337,7 +5715,7 @@ ${result.stderr ?? ""}`);
5337
5715
  let stub;
5338
5716
  try {
5339
5717
  stub = parseJsonc(
5340
- readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
5718
+ readFileSync14(join17(cwd, LINT_CONFIG_FILE), "utf8"),
5341
5719
  LINT_CONFIG_FILE
5342
5720
  );
5343
5721
  } catch {
@@ -5347,10 +5725,10 @@ ${result.stderr ?? ""}`);
5347
5725
  for (const entry of stub.extends ?? []) {
5348
5726
  try {
5349
5727
  const preset = parseJsonc(
5350
- readFileSync13(join16(cwd, entry), "utf8"),
5728
+ readFileSync14(join17(cwd, entry), "utf8"),
5351
5729
  entry
5352
5730
  );
5353
- for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
5731
+ for (const rule of Object.keys(preset.rules ?? {})) names.add(canonicalRuleName(rule));
5354
5732
  } catch {
5355
5733
  continue;
5356
5734
  }
@@ -5417,14 +5795,14 @@ ${result.stderr ?? ""}`);
5417
5795
  let parsed;
5418
5796
  try {
5419
5797
  parsed = parseJsonc(
5420
- readFileSync13(join16(cwd, LINT_CONFIG_FILE), "utf8"),
5798
+ readFileSync14(join17(cwd, LINT_CONFIG_FILE), "utf8"),
5421
5799
  LINT_CONFIG_FILE
5422
5800
  );
5423
5801
  } catch {
5424
5802
  return void 0;
5425
5803
  }
5426
5804
  const targets2 = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
5427
- return targets2.find((target) => !existsSync12(join16(cwd, target)));
5805
+ return targets2.find((target) => !existsSync12(join17(cwd, target)));
5428
5806
  }
5429
5807
  /** Announce what is not enforced, so reduced coverage is never silent. */
5430
5808
  announceDisabled(preset) {
@@ -5456,8 +5834,8 @@ function registerLint() {
5456
5834
 
5457
5835
  // src/roles/static-analysis/adapters/jscpd/jscpd.adapter.ts
5458
5836
  import { spawnSync as spawnSync6 } from "node:child_process";
5459
- import { existsSync as existsSync18, mkdirSync as mkdirSync2, readFileSync as readFileSync19, unlinkSync, writeFileSync as writeFileSync6 } from "node:fs";
5460
- import { join as join26 } from "node:path";
5837
+ import { existsSync as existsSync18, mkdirSync as mkdirSync2, readFileSync as readFileSync20, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
5838
+ import { join as join27 } from "node:path";
5461
5839
  import { globSync as globSync5 } from "tinyglobby";
5462
5840
 
5463
5841
  // src/roles/static-analysis/bundle/budgets.ts
@@ -5486,21 +5864,21 @@ function bandFor(valueKb, referenceKb) {
5486
5864
  }
5487
5865
 
5488
5866
  // src/roles/static-analysis/bundle/scan.ts
5489
- import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
5490
- import { basename, join as join18 } from "node:path";
5867
+ import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
5868
+ import { basename, join as join19 } from "node:path";
5491
5869
  import { pathToFileURL } from "node:url";
5492
5870
  import { gzipSync } from "node:zlib";
5493
5871
  import { globSync as globSync2 } from "tinyglobby";
5494
5872
 
5495
5873
  // src/roles/static-analysis/bundle/find-build.ts
5496
- import { existsSync as existsSync13, readFileSync as readFileSync14, statSync as statSync4 } from "node:fs";
5497
- import { join as join17 } from "node:path";
5874
+ import { existsSync as existsSync13, readFileSync as readFileSync15, statSync as statSync4 } from "node:fs";
5875
+ import { join as join18 } from "node:path";
5498
5876
  import { globSync } from "tinyglobby";
5499
5877
  var MANIFEST_GLOB = "server/_tanstack-start-manifest*.mjs";
5500
5878
  function candidateDirs(cwd) {
5501
5879
  const declared = readProjectPackageJson(cwd).nx?.targets?.build?.["outputs"];
5502
- const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) => join17(cwd, token.replace(/^\{projectRoot\}\//, ""))) : [];
5503
- return [...fromDeclaration, join17(cwd, ".output"), join17(cwd, "dist"), join17(cwd, "build")];
5880
+ const fromDeclaration = Array.isArray(declared) ? declared.filter((token) => typeof token === "string").map((token) => join18(cwd, token.replace(/^\{projectRoot\}\//, ""))) : [];
5881
+ return [...fromDeclaration, join18(cwd, ".output"), join18(cwd, "dist"), join18(cwd, "build")];
5504
5882
  }
5505
5883
  function findBuildOutput(cwd) {
5506
5884
  const lookedIn = [];
@@ -5510,7 +5888,7 @@ function findBuildOutput(cwd) {
5510
5888
  const found = globSync(MANIFEST_GLOB, { cwd: dir, absolute: true, dot: false });
5511
5889
  if (found.length === 0) continue;
5512
5890
  const manifestPath = found[0];
5513
- const publicDir = join17(dir, "public");
5891
+ const publicDir = join18(dir, "public");
5514
5892
  if (!existsSync13(publicDir)) continue;
5515
5893
  return { manifestPath, publicDir, ...dateOf(dir, manifestPath) };
5516
5894
  }
@@ -5533,10 +5911,10 @@ function findBuildOutput(cwd) {
5533
5911
  };
5534
5912
  }
5535
5913
  function dateOf(dir, manifestPath) {
5536
- const record = join17(dir, "nitro.json");
5914
+ const record = join18(dir, "nitro.json");
5537
5915
  if (existsSync13(record)) {
5538
5916
  try {
5539
- const parsed = JSON.parse(readFileSync14(record, "utf8"));
5917
+ const parsed = JSON.parse(readFileSync15(record, "utf8"));
5540
5918
  if (typeof parsed.date === "string") {
5541
5919
  const date = new Date(parsed.date);
5542
5920
  if (!Number.isNaN(date.getTime())) return { builtAt: date, builtAtFrom: "build record" };
@@ -5562,9 +5940,9 @@ function kindOf(path2) {
5562
5940
  return "other";
5563
5941
  }
5564
5942
  function resolveAnnounced(publicDir, announced) {
5565
- const direct = join18(publicDir, announced);
5943
+ const direct = join19(publicDir, announced);
5566
5944
  if (existsSync14(direct)) return direct;
5567
- const stripped = join18(publicDir, announced.replace(/^\/[^/]+\//, "/"));
5945
+ const stripped = join19(publicDir, announced.replace(/^\/[^/]+\//, "/"));
5568
5946
  return existsSync14(stripped) ? stripped : void 0;
5569
5947
  }
5570
5948
  function emptyWeight() {
@@ -5601,7 +5979,7 @@ async function scanBundle(cwd) {
5601
5979
  unresolved.push(announced);
5602
5980
  measured.set(announced, void 0);
5603
5981
  } else {
5604
- const buffer = readFileSync15(path2);
5982
+ const buffer = readFileSync16(path2);
5605
5983
  measured.set(announced, {
5606
5984
  parsed: buffer.length,
5607
5985
  transfer: gzipSync(buffer, { level: 9 }).length
@@ -5688,7 +6066,7 @@ function scanDynamicLayer(publicDir, references) {
5688
6066
  })) {
5689
6067
  const name = basename(path2);
5690
6068
  if (referenced.has(name)) continue;
5691
- const buffer = readFileSync15(path2);
6069
+ const buffer = readFileSync16(path2);
5692
6070
  const size = { parsed: buffer.length, transfer: gzipSync(buffer, { level: 9 }).length };
5693
6071
  add(all2, size);
5694
6072
  const group = groupOf(name);
@@ -5716,23 +6094,23 @@ function scanDynamicLayer(publicDir, references) {
5716
6094
  }
5717
6095
 
5718
6096
  // src/roles/static-analysis/sources.ts
5719
- import { readFileSync as readFileSync16 } from "node:fs";
5720
- import { join as join21 } from "node:path";
5721
- import { parseSync as parseSync2 } from "oxc-parser";
6097
+ import { readFileSync as readFileSync17 } from "node:fs";
6098
+ import { join as join22 } from "node:path";
6099
+ import { parseSync as parseSync3 } from "oxc-parser";
5722
6100
  import { globSync as globSync3 } from "tinyglobby";
5723
6101
 
5724
6102
  // src/roles/static-analysis/adapters/jscpd/tool-config.ts
5725
- import { existsSync as existsSync16, mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
6103
+ import { existsSync as existsSync16, mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
5726
6104
  import { tmpdir } from "node:os";
5727
- import { join as join20 } from "node:path";
6105
+ import { join as join21 } from "node:path";
5728
6106
 
5729
6107
  // src/roles/static-analysis/configs/resolve.ts
5730
6108
  import { existsSync as existsSync15 } from "node:fs";
5731
- import { join as join19 } from "node:path";
6109
+ import { join as join20 } from "node:path";
5732
6110
  import { pathToFileURL as pathToFileURL2 } from "node:url";
5733
6111
  var STATIC_CONFIG_FILE = "static-analysis.config.mjs";
5734
6112
  async function loadStaticConfig(cwd) {
5735
- const path2 = join19(cwd, STATIC_CONFIG_FILE);
6113
+ const path2 = join20(cwd, STATIC_CONFIG_FILE);
5736
6114
  if (!existsSync15(path2)) {
5737
6115
  return { config: defineStaticAnalysis(), from: null, unreadable: null };
5738
6116
  }
@@ -5779,7 +6157,7 @@ function exclusions(config, scope = "tests") {
5779
6157
  return [.../* @__PURE__ */ new Set([...config.ignore, ...OWN_ARTEFACTS, ...tests])];
5780
6158
  }
5781
6159
  function measuredRoots(cwd) {
5782
- return existsSync16(join20(cwd, "src")) ? ["src"] : ["."];
6160
+ return existsSync16(join21(cwd, "src")) ? ["src"] : ["."];
5783
6161
  }
5784
6162
  function fileSettings(config, scope, extraEntries = []) {
5785
6163
  return {
@@ -5809,9 +6187,9 @@ function fileSettings(config, scope, extraEntries = []) {
5809
6187
  };
5810
6188
  }
5811
6189
  function duplicationRun(config, paths, scope) {
5812
- const dir = mkdtempSync(join20(tmpdir(), "sentinel-static-"));
5813
- const configPath = join20(dir, "jscpd.json");
5814
- writeFileSync3(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
6190
+ const dir = mkdtempSync(join21(tmpdir(), "sentinel-static-"));
6191
+ const configPath = join21(dir, "jscpd.json");
6192
+ writeFileSync4(configPath, `${JSON.stringify(fileSettings(config, scope), null, 2)}
5815
6193
  `, "utf8");
5816
6194
  const args = [
5817
6195
  "--config",
@@ -5836,9 +6214,9 @@ function duplicationRun(config, paths, scope) {
5836
6214
  return { args, rawReportDir: dir, dispose: () => rmSync3(dir, { force: true, recursive: true }) };
5837
6215
  }
5838
6216
  function deadCodeRun(config, paths, extraEntries = []) {
5839
- const dir = mkdtempSync(join20(tmpdir(), "sentinel-static-"));
5840
- const configPath = join20(dir, "jscpd.json");
5841
- writeFileSync3(
6217
+ const dir = mkdtempSync(join21(tmpdir(), "sentinel-static-"));
6218
+ const configPath = join21(dir, "jscpd.json");
6219
+ writeFileSync4(
5842
6220
  configPath,
5843
6221
  `${JSON.stringify(fileSettings(config, "dead-code", extraEntries), null, 2)}
5844
6222
  `,
@@ -5889,12 +6267,12 @@ function readParsed(cwd, files, passes) {
5889
6267
  function parsedSource(cwd, path2) {
5890
6268
  let text;
5891
6269
  try {
5892
- text = readFileSync16(join21(cwd, path2), "utf8");
6270
+ text = readFileSync17(join22(cwd, path2), "utf8");
5893
6271
  } catch {
5894
6272
  return void 0;
5895
6273
  }
5896
6274
  try {
5897
- const parsed = parseSync2(path2, text);
6275
+ const parsed = parseSync3(path2, text);
5898
6276
  if (parsed.errors.length > 0) return void 0;
5899
6277
  return { path: path2, text, program: parsed.program };
5900
6278
  } catch {
@@ -5903,7 +6281,7 @@ function parsedSource(cwd, path2) {
5903
6281
  }
5904
6282
 
5905
6283
  // src/roles/static-analysis/complexity/cyclomatic.ts
5906
- import { parseSync as parseSync3 } from "oxc-parser";
6284
+ import { parseSync as parseSync4 } from "oxc-parser";
5907
6285
  var FUNCTION_NODES = /* @__PURE__ */ new Set([
5908
6286
  "FunctionDeclaration",
5909
6287
  "FunctionExpression",
@@ -6076,13 +6454,13 @@ function unreadableNote(coverage) {
6076
6454
  }
6077
6455
 
6078
6456
  // src/roles/static-analysis/excerpt.ts
6079
- import { readFileSync as readFileSync17 } from "node:fs";
6080
- import { join as join23 } from "node:path";
6457
+ import { readFileSync as readFileSync18 } from "node:fs";
6458
+ import { join as join24 } from "node:path";
6081
6459
 
6082
6460
  // src/roles/static-analysis/maintainability/size-rules.ts
6083
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
6461
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
6084
6462
  import { tmpdir as tmpdir2 } from "node:os";
6085
- import { join as join22 } from "node:path";
6463
+ import { join as join23 } from "node:path";
6086
6464
  var GAUGE_THRESHOLDS = {
6087
6465
  // McCabe 1976 says 10, our p90 says 11. The only indicator where two independent candidates agree
6088
6466
  // within one unit, and 94.8% of our functions already sit in the 1-10 band SEI calls low risk.
@@ -6111,7 +6489,7 @@ var GAUGE_THRESHOLDS = {
6111
6489
  "cognitive-complexity": 7
6112
6490
  };
6113
6491
  var COGNITIVE = "cognitive-complexity";
6114
- var cognitivePlugin = () => join22(ownPackageDir(), "plugins", "sonar-cognitive.js");
6492
+ var cognitivePlugin = () => join23(ownPackageDir(), "plugins", "sonar-cognitive.js");
6115
6493
  var CATEGORIES = [
6116
6494
  "correctness",
6117
6495
  "suspicious",
@@ -6143,9 +6521,9 @@ function sizeRulesConfig() {
6143
6521
  };
6144
6522
  }
6145
6523
  function sizeRulesRun(paths, ignore) {
6146
- const dir = mkdtempSync2(join22(tmpdir2(), "sentinel-size-"));
6147
- const configPath = join22(dir, "oxlintrc.json");
6148
- writeFileSync4(configPath, `${JSON.stringify(sizeRulesConfig(), null, 2)}
6524
+ const dir = mkdtempSync2(join23(tmpdir2(), "sentinel-size-"));
6525
+ const configPath = join23(dir, "oxlintrc.json");
6526
+ writeFileSync5(configPath, `${JSON.stringify(sizeRulesConfig(), null, 2)}
6149
6527
  `, "utf8");
6150
6528
  return {
6151
6529
  // `--format json` because the console shape is rendered by this role from the figures: a
@@ -7602,7 +7980,7 @@ function linesOf(cache, cwd, relative5) {
7602
7980
  if (cache.has(relative5)) return cache.get(relative5);
7603
7981
  let lines;
7604
7982
  try {
7605
- lines = readFileSync17(join23(cwd, relative5), "utf8").split("\n");
7983
+ lines = readFileSync18(join24(cwd, relative5), "utf8").split("\n");
7606
7984
  } catch {
7607
7985
  lines = void 0;
7608
7986
  }
@@ -7834,7 +8212,7 @@ var SCRIPT = `(function(){
7834
8212
  })();`;
7835
8213
 
7836
8214
  // src/roles/static-analysis/graph/imports.ts
7837
- import { dirname, join as join24 } from "node:path";
8215
+ import { dirname, join as join25 } from "node:path";
7838
8216
  var TS_BEHIND_JS = {
7839
8217
  ".js": [".ts", ".tsx"],
7840
8218
  ".mjs": [".mts"],
@@ -7887,7 +8265,7 @@ function specifiersOf(program) {
7887
8265
  }
7888
8266
  function resolveSpecifier(from, specifier, known) {
7889
8267
  if (!specifier.startsWith(".")) return void 0;
7890
- const base = join24(dirname(from), specifier);
8268
+ const base = join25(dirname(from), specifier);
7891
8269
  if (known.has(base)) return base;
7892
8270
  for (const extension of RESOLVABLE_EXTENSIONS) {
7893
8271
  if (known.has(base + extension)) return base + extension;
@@ -7898,7 +8276,7 @@ function resolveSpecifier(from, specifier, known) {
7898
8276
  if (known.has(swapped)) return swapped;
7899
8277
  }
7900
8278
  for (const extension of RESOLVABLE_EXTENSIONS) {
7901
- const index = join24(base, `index${extension}`);
8279
+ const index = join25(base, `index${extension}`);
7902
8280
  if (known.has(index)) return index;
7903
8281
  }
7904
8282
  return void 0;
@@ -8011,26 +8389,26 @@ function scanSyntax(cwd, config) {
8011
8389
 
8012
8390
  // src/roles/static-analysis/workspace/consumers.ts
8013
8391
  import { spawnSync as spawnSync5 } from "node:child_process";
8014
- import { existsSync as existsSync17, mkdirSync, readFileSync as readFileSync18, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
8015
- import { dirname as dirname2, join as join25, relative as relative4, resolve as resolve3, sep } from "node:path";
8016
- var CACHE_DIR = join25("node_modules", ".cache", "sentinel");
8392
+ import { existsSync as existsSync17, mkdirSync, readFileSync as readFileSync19, statSync as statSync5, writeFileSync as writeFileSync6 } from "node:fs";
8393
+ import { dirname as dirname2, join as join26, relative as relative4, resolve as resolve3, sep } from "node:path";
8394
+ var CACHE_DIR = join26("node_modules", ".cache", "sentinel");
8017
8395
  var CACHE_FILE = "consumer-index.json";
8018
8396
  var SHAPE = 4;
8019
8397
  function workspaceRootFrom(cwd) {
8020
8398
  let dir = resolve3(cwd);
8021
8399
  for (; ; ) {
8022
- if (existsSync17(join25(dir, WORKSPACE_ROOT_MARKER))) return dir;
8400
+ if (existsSync17(join26(dir, WORKSPACE_ROOT_MARKER))) return dir;
8023
8401
  const parent = dirname2(dir);
8024
8402
  if (parent === dir) return void 0;
8025
8403
  dir = parent;
8026
8404
  }
8027
8405
  }
8028
8406
  function aliasTable(workspaceRoot) {
8029
- const file = join25(workspaceRoot, "tsconfig.base.json");
8407
+ const file = join26(workspaceRoot, "tsconfig.base.json");
8030
8408
  if (!existsSync17(file)) return /* @__PURE__ */ new Map();
8031
8409
  let paths;
8032
8410
  try {
8033
- const parsed = JSON.parse(readFileSync18(file, "utf8"));
8411
+ const parsed = JSON.parse(readFileSync19(file, "utf8"));
8034
8412
  paths = parsed.compilerOptions?.paths ?? {};
8035
8413
  } catch {
8036
8414
  return /* @__PURE__ */ new Map();
@@ -8048,7 +8426,7 @@ function aliasTable(workspaceRoot) {
8048
8426
  function resolveSpecifier2(specifier, table, workspaceRoot) {
8049
8427
  const isFile = (candidate) => {
8050
8428
  try {
8051
- return statSync5(join25(workspaceRoot, candidate)).isFile();
8429
+ return statSync5(join26(workspaceRoot, candidate)).isFile();
8052
8430
  } catch {
8053
8431
  return false;
8054
8432
  }
@@ -8085,11 +8463,11 @@ function resolveSpecifier2(specifier, table, workspaceRoot) {
8085
8463
  return void 0;
8086
8464
  }
8087
8465
  function resolveRelative(importer, specifier, workspaceRoot) {
8088
- const base = join25(dirname2(importer), specifier).split(sep).join("/");
8466
+ const base = join26(dirname2(importer), specifier).split(sep).join("/");
8089
8467
  if (base.startsWith("..")) return void 0;
8090
8468
  const isFile = (candidate) => {
8091
8469
  try {
8092
- return statSync5(join25(workspaceRoot, candidate)).isFile();
8470
+ return statSync5(join26(workspaceRoot, candidate)).isFile();
8093
8471
  } catch {
8094
8472
  return false;
8095
8473
  }
@@ -8187,10 +8565,10 @@ function hash(text) {
8187
8565
  }
8188
8566
  function consumerIndex(workspaceRoot, moduleRoots) {
8189
8567
  const key = cacheKey(workspaceRoot);
8190
- const file = join25(workspaceRoot, CACHE_DIR, CACHE_FILE);
8568
+ const file = join26(workspaceRoot, CACHE_DIR, CACHE_FILE);
8191
8569
  if (key && existsSync17(file)) {
8192
8570
  try {
8193
- const cached = JSON.parse(readFileSync18(file, "utf8"));
8571
+ const cached = JSON.parse(readFileSync19(file, "utf8"));
8194
8572
  if (cached.key === key) {
8195
8573
  return {
8196
8574
  imported: new Set(cached.imported),
@@ -8204,14 +8582,14 @@ function consumerIndex(workspaceRoot, moduleRoots) {
8204
8582
  const index = buildConsumerIndex(workspaceRoot, moduleRoots());
8205
8583
  if (key) {
8206
8584
  try {
8207
- mkdirSync(join25(workspaceRoot, CACHE_DIR), { recursive: true });
8585
+ mkdirSync(join26(workspaceRoot, CACHE_DIR), { recursive: true });
8208
8586
  const payload = {
8209
8587
  key,
8210
8588
  imported: [...index.imported],
8211
8589
  testOnly: [...index.testOnly],
8212
8590
  builtFrom: index.builtFrom
8213
8591
  };
8214
- writeFileSync5(file, JSON.stringify(payload), "utf8");
8592
+ writeFileSync6(file, JSON.stringify(payload), "utf8");
8215
8593
  } catch {
8216
8594
  }
8217
8595
  }
@@ -8308,7 +8686,7 @@ var JscpdAdapter = class extends BaseAdapter {
8308
8686
  operations: [
8309
8687
  // `write` and not `merge-json`: this file is code, and the engine's in-place merge is for
8310
8688
  // JSON. A re-init therefore RESTORES the stub, so an existing one is left alone below.
8311
- ...existsSync18(join26(context2.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: configStub() }],
8689
+ ...existsSync18(join27(context2.cwd, STATIC_CONFIG_FILE)) ? [] : [{ kind: "write", path: STATIC_CONFIG_FILE, contents: configStub() }],
8312
8690
  manifestOperation(context2.cwd, scripts()),
8313
8691
  ...nxTargetOperations({ cwd: context2.cwd, targets: targets() })
8314
8692
  // The report directory is ignored ONCE at the workspace root, by `--init`'s workspace prep,
@@ -8558,7 +8936,7 @@ var JscpdAdapter = class extends BaseAdapter {
8558
8936
  const declared = moduleScripts(ctx.cwd);
8559
8937
  const missing = Object.keys(scripts()).filter((name) => !(name in declared));
8560
8938
  const adopted = missing.length < Object.keys(scripts()).length;
8561
- const hasConfig = existsSync18(join26(ctx.cwd, STATIC_CONFIG_FILE));
8939
+ const hasConfig = existsSync18(join27(ctx.cwd, STATIC_CONFIG_FILE));
8562
8940
  return {
8563
8941
  adopted,
8564
8942
  preset: adopted ? ctx.preset : null,
@@ -8650,7 +9028,7 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
8650
9028
  try {
8651
9029
  const ran = runTool(jscpd, args, cwd, "dead-code");
8652
9030
  if (ran.kind !== "ran") return ran;
8653
- const path2 = join26(rawReportDir, RAW_REPORT["dead-code"]);
9031
+ const path2 = join27(rawReportDir, RAW_REPORT["dead-code"]);
8654
9032
  if (!existsSync18(path2)) {
8655
9033
  process.stderr.write(
8656
9034
  `sentinel static-analysis(jscpd): the dead-code pass wrote no ${RAW_REPORT["dead-code"]}
@@ -8658,7 +9036,7 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
8658
9036
  );
8659
9037
  return { kind: "failed", code: 1 };
8660
9038
  }
8661
- const parsed = safeParseDeadCode(readFileSync19(path2, "utf8"));
9039
+ const parsed = safeParseDeadCode(readFileSync20(path2, "utf8"));
8662
9040
  const statistics = parsed?.statistics;
8663
9041
  if (!statistics) {
8664
9042
  process.stderr.write(
@@ -8681,11 +9059,11 @@ function runDeadCode(jscpd, config, paths, cwd, extraEntries = []) {
8681
9059
  }
8682
9060
  }
8683
9061
  function moduleRelative(name, roots, cwd) {
8684
- if (existsSync18(join26(cwd, name))) return name;
9062
+ if (existsSync18(join27(cwd, name))) return name;
8685
9063
  for (const root of roots) {
8686
9064
  if (root === ".") continue;
8687
9065
  const candidate = `${root}/${name}`;
8688
- if (existsSync18(join26(cwd, candidate))) return candidate;
9066
+ if (existsSync18(join27(cwd, candidate))) return candidate;
8689
9067
  }
8690
9068
  return name;
8691
9069
  }
@@ -8694,8 +9072,8 @@ function runDuplication(jscpd, config, paths, scope, cwd) {
8694
9072
  try {
8695
9073
  const ran = runTool(jscpd, args, cwd, scope);
8696
9074
  if (ran.kind !== "ran") return ran;
8697
- const path2 = join26(rawReportDir, RAW_REPORT.duplication);
8698
- const parsed = existsSync18(path2) ? safeParse(readFileSync19(path2, "utf8")) : void 0;
9075
+ const path2 = join27(rawReportDir, RAW_REPORT.duplication);
9076
+ const parsed = existsSync18(path2) ? safeParse(readFileSync20(path2, "utf8")) : void 0;
8699
9077
  const total = parsed?.statistics?.total;
8700
9078
  if (!total) {
8701
9079
  process.stderr.write(
@@ -8864,7 +9242,7 @@ function outsideOf(cwd) {
8864
9242
  function moduleRootsOf(workspaceRoot) {
8865
9243
  const roots = [];
8866
9244
  for (const area of ["apps", "libs"]) {
8867
- const base = join26(workspaceRoot, area);
9245
+ const base = join27(workspaceRoot, area);
8868
9246
  if (!existsSync18(base)) continue;
8869
9247
  for (const entry of globSync5([`${area}/**/package.json`, `${area}/**/project.json`], {
8870
9248
  cwd: workspaceRoot,
@@ -9207,7 +9585,7 @@ var PICTURE_FILE = `${TARGET}-graph.svg`;
9207
9585
  function clearStaleOutputs(cwd, keeping) {
9208
9586
  for (const name of [...Object.values(SHAPE_FILE), PICTURE_FILE]) {
9209
9587
  if (keeping.has(name)) continue;
9210
- const path2 = join26(cwd, REPORT_DIR, name);
9588
+ const path2 = join27(cwd, REPORT_DIR, name);
9211
9589
  if (existsSync18(path2)) unlinkSync(path2);
9212
9590
  }
9213
9591
  }
@@ -9226,11 +9604,11 @@ function drawsPicture(graph, coverage) {
9226
9604
  async function emit(report, shape, cwd, graph, coverage) {
9227
9605
  if (shape === "console") return [];
9228
9606
  const detailed = shape === "markdown" ? report : withExcerpts(report, cwd);
9229
- mkdirSync2(join26(cwd, REPORT_DIR), { recursive: true });
9607
+ mkdirSync2(join27(cwd, REPORT_DIR), { recursive: true });
9230
9608
  const written = [];
9231
9609
  if (shape === "html" && drawsPicture(graph, coverage)) {
9232
9610
  const drawn = layOut(graph.graph, graph.communityOf);
9233
- const path2 = join26(cwd, REPORT_DIR, PICTURE_FILE);
9611
+ const path2 = join27(cwd, REPORT_DIR, PICTURE_FILE);
9234
9612
  await writeGraphPicture(drawn, path2);
9235
9613
  written.push(`${REPORT_DIR}/${PICTURE_FILE}`);
9236
9614
  detailed.picture = buildExplorer(drawn, graph.communityOf).markup;
@@ -9238,7 +9616,7 @@ async function emit(report, shape, cwd, graph, coverage) {
9238
9616
  const RENDER = { json: renderJson, html: renderHtml, markdown: renderMarkdown };
9239
9617
  const name = SHAPE_FILE[shape] ?? `${TARGET}.${shape}`;
9240
9618
  const body = RENDER[shape](detailed);
9241
- writeFileSync6(join26(cwd, REPORT_DIR, name), body, "utf8");
9619
+ writeFileSync7(join27(cwd, REPORT_DIR, name), body, "utf8");
9242
9620
  written.unshift(`${REPORT_DIR}/${name}`);
9243
9621
  return written;
9244
9622
  }
@@ -9272,9 +9650,9 @@ function registerTest() {
9272
9650
 
9273
9651
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
9274
9652
  import { spawnSync as spawnSync7 } from "node:child_process";
9275
- import { existsSync as existsSync19, readFileSync as readFileSync21 } from "node:fs";
9653
+ import { existsSync as existsSync19, readFileSync as readFileSync22 } from "node:fs";
9276
9654
  import { createRequire } from "node:module";
9277
- import { join as join28 } from "node:path";
9655
+ import { join as join29 } from "node:path";
9278
9656
 
9279
9657
  // src/roles/typescript/presets/nest.json
9280
9658
  var nest_default2 = {
@@ -9459,8 +9837,8 @@ function readTsconfigAdoption(cwd) {
9459
9837
  }
9460
9838
 
9461
9839
  // src/roles/typescript/adapters/tsc/plan.ts
9462
- import { readFileSync as readFileSync20 } from "node:fs";
9463
- import { join as join27 } from "node:path";
9840
+ import { readFileSync as readFileSync21 } from "node:fs";
9841
+ import { join as join28 } from "node:path";
9464
9842
 
9465
9843
  // src/roles/typescript/typecheck-script.ts
9466
9844
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -9553,7 +9931,7 @@ function planAdoption(context2) {
9553
9931
  };
9554
9932
  }
9555
9933
  const existing = parseJsonc(
9556
- readFileSync20(join27(context2.cwd, target.path), "utf8"),
9934
+ readFileSync21(join28(context2.cwd, target.path), "utf8"),
9557
9935
  target.path
9558
9936
  );
9559
9937
  const extendsChain = composeExtends(existing.extends, preset);
@@ -9691,7 +10069,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9691
10069
  let chain;
9692
10070
  try {
9693
10071
  const parsed = parseJsonc(
9694
- readFileSync21(join28(cwd, target.path), "utf8"),
10072
+ readFileSync22(join29(cwd, target.path), "utf8"),
9695
10073
  target.path
9696
10074
  );
9697
10075
  chain = parsed.extends;
@@ -9704,7 +10082,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9704
10082
  );
9705
10083
  if (preset === void 0) return void 0;
9706
10084
  try {
9707
- createRequire(join28(cwd, "noop.js")).resolve(preset);
10085
+ createRequire(join29(cwd, "noop.js")).resolve(preset);
9708
10086
  return void 0;
9709
10087
  } catch {
9710
10088
  return preset;
@@ -9800,7 +10178,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9800
10178
  referencedProjects(cwd, config) {
9801
10179
  try {
9802
10180
  const parsed = parseJsonc(
9803
- readFileSync21(join28(cwd, config), "utf8"),
10181
+ readFileSync22(join29(cwd, config), "utf8"),
9804
10182
  config
9805
10183
  );
9806
10184
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path2) => typeof path2 === "string" && path2.length > 0);
@@ -9816,7 +10194,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
9816
10194
  * check.
9817
10195
  */
9818
10196
  typecheckTarget(cwd) {
9819
- if (existsSync19(join28(cwd, "tsconfig.json"))) return "tsconfig.json";
10197
+ if (existsSync19(join29(cwd, "tsconfig.json"))) return "tsconfig.json";
9820
10198
  const target = resolveTsconfigTarget(cwd);
9821
10199
  return target.reason === "none" ? null : target.path;
9822
10200
  }
@@ -10022,7 +10400,7 @@ function replaceLines(current, replacements) {
10022
10400
  }
10023
10401
 
10024
10402
  // src/core/apply-plan.ts
10025
- import { existsSync as existsSync20, readFileSync as readFileSync22, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
10403
+ import { existsSync as existsSync20, readFileSync as readFileSync23, renameSync, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
10026
10404
  import { resolve as resolve4, sep as sep2 } from "node:path";
10027
10405
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
10028
10406
  function resolveWithinRoot(cwd, relativePath) {
@@ -10034,7 +10412,7 @@ function resolveWithinRoot(cwd, relativePath) {
10034
10412
  return absolutePath;
10035
10413
  }
10036
10414
  function readIfExists(absolutePath) {
10037
- return existsSync20(absolutePath) ? readFileSync22(absolutePath, "utf8") : void 0;
10415
+ return existsSync20(absolutePath) ? readFileSync23(absolutePath, "utf8") : void 0;
10038
10416
  }
10039
10417
  function* leaves(value, prefix = []) {
10040
10418
  for (const [key, keyValue] of Object.entries(value)) {
@@ -10136,7 +10514,7 @@ function preparePlan(cwd, plan3) {
10136
10514
  }
10137
10515
  function writeFileAtomic(absolutePath, contents) {
10138
10516
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
10139
- writeFileSync7(tempPath, contents);
10517
+ writeFileSync8(tempPath, contents);
10140
10518
  renameSync(tempPath, absolutePath);
10141
10519
  }
10142
10520
  function applyPlan(cwd, plan3) {
@@ -10152,8 +10530,8 @@ function applyPlan(cwd, plan3) {
10152
10530
  }
10153
10531
 
10154
10532
  // src/core/config/preset-evidence.ts
10155
- import { existsSync as existsSync21, readdirSync as readdirSync3, readFileSync as readFileSync23 } from "node:fs";
10156
- import { join as join29 } from "node:path";
10533
+ import { existsSync as existsSync21, readdirSync as readdirSync3, readFileSync as readFileSync24 } from "node:fs";
10534
+ import { join as join30 } from "node:path";
10157
10535
  var PATH_SIGNALS = [
10158
10536
  {
10159
10537
  preset: "nest",
@@ -10187,12 +10565,12 @@ function sourceImports(cwd, pattern) {
10187
10565
  }
10188
10566
  for (const entry of entries) {
10189
10567
  if (entry.isDirectory()) {
10190
- if (!NOT_SCANNED.has(entry.name)) stack.push(join29(dir, entry.name));
10568
+ if (!NOT_SCANNED.has(entry.name)) stack.push(join30(dir, entry.name));
10191
10569
  continue;
10192
10570
  }
10193
10571
  if (!SOURCE_FILE.test(entry.name)) continue;
10194
10572
  try {
10195
- if (pattern.test(readFileSync23(join29(dir, entry.name), "utf8"))) return true;
10573
+ if (pattern.test(readFileSync24(join30(dir, entry.name), "utf8"))) return true;
10196
10574
  } catch {
10197
10575
  continue;
10198
10576
  }
@@ -10201,11 +10579,11 @@ function sourceImports(cwd, pattern) {
10201
10579
  return false;
10202
10580
  }
10203
10581
  function dependencyNames(cwd) {
10204
- const path2 = join29(cwd, "package.json");
10582
+ const path2 = join30(cwd, "package.json");
10205
10583
  if (!existsSync21(path2)) return [];
10206
10584
  try {
10207
10585
  const manifest = parseJsonc(
10208
- readFileSync23(path2, "utf8"),
10586
+ readFileSync24(path2, "utf8"),
10209
10587
  path2
10210
10588
  );
10211
10589
  return [
@@ -10228,7 +10606,7 @@ function reactTsconfigEvidence(cwd) {
10228
10606
  }
10229
10607
  for (const name of entries) {
10230
10608
  try {
10231
- const config = parseJsonc(readFileSync23(join29(cwd, name), "utf8"), name);
10609
+ const config = parseJsonc(readFileSync24(join30(cwd, name), "utf8"), name);
10232
10610
  if (config.compilerOptions?.jsx !== void 0) return 'its tsconfig sets "jsx"';
10233
10611
  const extended = Array.isArray(config.extends) ? config.extends : config.extends === void 0 ? [] : [config.extends];
10234
10612
  if (extended.some((entry) => entry === REACT_TYPESCRIPT_PRESET)) {
@@ -10329,8 +10707,10 @@ async function dispatch(opts) {
10329
10707
  }
10330
10708
  const detected = resolveFlavour(opts);
10331
10709
  const resolutionPreset = opts.preset ?? declaredPresetFor(opts.target, opts.cwd) ?? detected;
10332
- if (resolutionPreset === "tools") {
10333
- 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.`;
10710
+ const declaredTools = opts.preset === void 0 ? availableTargets().find((target) => declaredPresetFor(target, opts.cwd) === "tools") : void 0;
10711
+ if (resolutionPreset === "tools" || declaredTools !== void 0) {
10712
+ const declaredBy = declaredTools === void 0 || declaredTools === opts.target ? "" : `this module's ${declaredTools} config declares the tools preset, and `;
10713
+ 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.`;
10334
10714
  process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${message}`)}
10335
10715
  `);
10336
10716
  opts.record?.({ role: opts.target, status: "deferred", reason: message });