@hublo/sentinel 1.1.4 → 1.1.6

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.
@@ -9,6 +9,7 @@ import {
9
9
  dispatch,
10
10
  ensureWorkspacePrep,
11
11
  findWorkspaceRoot,
12
+ inspectWorkspacePrep,
12
13
  palette,
13
14
  readNxProjectName,
14
15
  readOwnVersion,
@@ -16,7 +17,7 @@ import {
16
17
  registerAdapters,
17
18
  resolve,
18
19
  resolveBin
19
- } from "../chunk-HNE774F7.js";
20
+ } from "../chunk-RGIEWANO.js";
20
21
 
21
22
  // bin/sentinel.ts
22
23
  import { program } from "commander";
@@ -438,6 +439,10 @@ function renderStatusSummary(items, p) {
438
439
  }
439
440
 
440
441
  // src/cli/run-verb.ts
442
+ function workspacePrep(cwd2) {
443
+ const root = findWorkspaceRoot(cwd2);
444
+ return root ? inspectWorkspacePrep(root) : [];
445
+ }
441
446
  async function runVerb(ctx) {
442
447
  const { modules, scope } = resolveContext(ctx.cwd, { module: ctx.module, ci: ctx.ci });
443
448
  const out = palette(process.stdout);
@@ -482,7 +487,15 @@ async function runVerb(ctx) {
482
487
  });
483
488
  const summary = generateSummaries(results, ctx.targets);
484
489
  if (ctx.json) {
485
- const base = ctx.verb === "status" || ctx.verb === "inspect" ? { ...summary, coverage: statusCoverage(summary.results) } : summary;
490
+ const base = ctx.verb === "status" || ctx.verb === "inspect" ? {
491
+ ...summary,
492
+ coverage: statusCoverage(summary.results),
493
+ // The root-level changes sentinel maintains, each with its justification checked
494
+ // NOW rather than described in a comment. They are the only thing sentinel does
495
+ // that nobody can see afterwards, and every one of them is temporary. See
496
+ // `inspectWorkspacePrep`.
497
+ ...ctx.verb === "inspect" ? { workspace: workspacePrep(ctx.cwd) } : {}
498
+ } : summary;
486
499
  const payload = ctx.toolArgs.length > 0 ? { ...base, toolArgs: ctx.toolArgs } : base;
487
500
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
488
501
  } else if (ctx.verb === "status" || ctx.verb === "inspect" && modules.length > 1) {
@@ -493,6 +506,19 @@ async function runVerb(ctx) {
493
506
  process.stdout.write(renderSummary(item, out) + "\n");
494
507
  }
495
508
  }
509
+ if (!ctx.json && ctx.verb === "inspect") {
510
+ const prep = workspacePrep(ctx.cwd);
511
+ if (prep.length > 0) {
512
+ process.stdout.write(`
513
+ sentinel maintains these at the workspace root:
514
+ `);
515
+ for (const entry of prep) {
516
+ process.stdout.write(` ${out.strong(entry.rule)}
517
+ ${entry.reason}
518
+ `);
519
+ }
520
+ }
521
+ }
496
522
  process.stderr.write(
497
523
  err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms
498
524
  `)
@@ -140,12 +140,106 @@ var PRESET_NAMES = ["react", "nest", "svelte", "node", "tools"];
140
140
 
141
141
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
142
142
  import { spawnSync } from "child_process";
143
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
144
- import { join as join11 } from "path";
143
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
144
+ import { join as join12 } from "path";
145
+
146
+ // src/core/config/existing-command.ts
147
+ import { readFileSync as readFileSync3 } from "fs";
148
+ import { join as join3 } from "path";
149
+
150
+ // src/shared/jsonc.ts
151
+ import { parse, printParseErrorCode } from "jsonc-parser";
152
+ function parseJsonc(text, source = "config") {
153
+ const errors = [];
154
+ const value = parse(text, errors, { allowTrailingComma: true });
155
+ if (errors.length > 0) {
156
+ const details = errors.map((error) => printParseErrorCode(error.error)).join(", ");
157
+ throw new Error(`${source}: malformed JSONC (${details}).`);
158
+ }
159
+ return value;
160
+ }
161
+
162
+ // src/core/config/manifest.ts
163
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
164
+ import { basename, join as join2 } from "path";
165
+ function moduleScripts(cwd) {
166
+ try {
167
+ const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
168
+ return pkg.scripts ?? {};
169
+ } catch {
170
+ return {};
171
+ }
172
+ }
173
+ function moduleName(cwd) {
174
+ try {
175
+ const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
176
+ return pkg.name;
177
+ } catch {
178
+ return void 0;
179
+ }
180
+ }
181
+ function isSelfAdoption(cwd) {
182
+ return moduleName(cwd) === readOwnPackage().name;
183
+ }
184
+ function selfCommand(cwd, flags) {
185
+ const own = readOwnPackage();
186
+ if (!isSelfAdoption(cwd) || !own.bin) return void 0;
187
+ return `node ${own.bin.replace(/^\.\//, "")} ${flags}`;
188
+ }
189
+ function manifestOperation(cwd, scripts) {
190
+ const own = readOwnPackage();
191
+ const value = {
192
+ scripts,
193
+ devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
194
+ };
195
+ if (existsSync2(join2(cwd, "package.json"))) {
196
+ return { kind: "merge-json", path: "package.json", value };
197
+ }
198
+ return {
199
+ kind: "merge-json",
200
+ path: "package.json",
201
+ value: { name: readNxProjectName(cwd) ?? basename(cwd), private: true, ...value }
202
+ };
203
+ }
204
+
205
+ // src/core/config/existing-command.ts
206
+ function nxTargetCommand(cwd, target) {
207
+ let project;
208
+ try {
209
+ project = parseJsonc(
210
+ readFileSync3(join3(cwd, "project.json"), "utf8"),
211
+ "project.json"
212
+ );
213
+ } catch {
214
+ return void 0;
215
+ }
216
+ const entry = project.targets?.[target];
217
+ if (!entry || entry.executor !== "nx:run-commands") return void 0;
218
+ const { command, commands, cwd: targetCwd } = entry.options ?? {};
219
+ const ranInModule = typeof targetCwd === "string" && targetCwd.trim() !== "";
220
+ if (typeof command === "string" && command.trim() !== "") {
221
+ return { command: command.trim(), ranInModule };
222
+ }
223
+ if (Array.isArray(commands)) {
224
+ const parts = commands.filter((c) => typeof c === "string" && c.trim() !== "");
225
+ if (parts.length > 0) return { command: parts.map((c) => c.trim()).join(" && "), ranInModule };
226
+ }
227
+ return void 0;
228
+ }
229
+ function rootedForScript(command) {
230
+ return command.replace(/(^|&&\s*)pnpm exec /g, "$1pnpm --workspace-root exec ");
231
+ }
232
+ function existingCommand(cwd, target) {
233
+ const script = moduleScripts(cwd)[target];
234
+ if (script !== void 0 && script.trim() !== "") return script;
235
+ const fromTarget = nxTargetCommand(cwd, target);
236
+ if (fromTarget === void 0) return void 0;
237
+ return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
238
+ }
145
239
 
146
240
  // src/core/config/has-source.ts
147
241
  import { readdirSync } from "fs";
148
- import { extname, join as join2 } from "path";
242
+ import { extname, join as join4 } from "path";
149
243
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
150
244
  "node_modules",
151
245
  "dist",
@@ -196,7 +290,7 @@ function hasSourceFiles(cwd, extensions) {
196
290
  }
197
291
  for (const entry of entries) {
198
292
  if (entry.isDirectory()) {
199
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join2(dir, entry.name));
293
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join4(dir, entry.name));
200
294
  continue;
201
295
  }
202
296
  if (wanted.has(extname(entry.name))) return true;
@@ -205,58 +299,15 @@ function hasSourceFiles(cwd, extensions) {
205
299
  return false;
206
300
  }
207
301
 
208
- // src/core/config/manifest.ts
209
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
210
- import { basename, join as join3 } from "path";
211
- function moduleScripts(cwd) {
212
- try {
213
- const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
214
- return pkg.scripts ?? {};
215
- } catch {
216
- return {};
217
- }
218
- }
219
- function moduleName(cwd) {
220
- try {
221
- const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
222
- return pkg.name;
223
- } catch {
224
- return void 0;
225
- }
226
- }
227
- function isSelfAdoption(cwd) {
228
- return moduleName(cwd) === readOwnPackage().name;
229
- }
230
- function selfCommand(cwd, flags) {
231
- const own = readOwnPackage();
232
- if (!isSelfAdoption(cwd) || !own.bin) return void 0;
233
- return `node ${own.bin.replace(/^\.\//, "")} ${flags}`;
234
- }
235
- function manifestOperation(cwd, scripts) {
236
- const own = readOwnPackage();
237
- const value = {
238
- scripts,
239
- devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
240
- };
241
- if (existsSync2(join3(cwd, "package.json"))) {
242
- return { kind: "merge-json", path: "package.json", value };
243
- }
244
- return {
245
- kind: "merge-json",
246
- path: "package.json",
247
- value: { name: readNxProjectName(cwd) ?? basename(cwd), private: true, ...value }
248
- };
249
- }
250
-
251
302
  // src/core/config/nx-target.ts
252
303
  import { existsSync as existsSync3 } from "fs";
253
- import { join as join4 } from "path";
304
+ import { join as join5 } from "path";
254
305
  function nxTargetOperations(options) {
255
306
  const { cwd, targets } = options;
256
307
  const names = Object.keys(targets);
257
308
  if (names.length === 0) return [];
258
309
  const operations = [];
259
- if (existsSync3(join4(cwd, "project.json"))) {
310
+ if (existsSync3(join5(cwd, "project.json"))) {
260
311
  operations.push({
261
312
  kind: "remove-json-keys",
262
313
  path: "project.json",
@@ -294,8 +345,8 @@ var WORKSPACE_ROOT_MARKER = "nx.json";
294
345
  var DEFAULT_MAX_DIAGNOSTICS = 100;
295
346
 
296
347
  // src/core/workspace-prep.ts
297
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync } from "fs";
298
- import { dirname as dirname2, join as join5, relative } from "path";
348
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync } from "fs";
349
+ import { dirname as dirname2, join as join6, relative } from "path";
299
350
  var OVERRIDE_KEY = "i18next>typescript";
300
351
  var NATIVE_TS_ALIAS = "@typescript/native";
301
352
  var WORKSPACE_YAML = "pnpm-workspace.yaml";
@@ -304,7 +355,7 @@ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
304
355
  function findWorkspaceRoot(startDir) {
305
356
  let dir = startDir;
306
357
  for (; ; ) {
307
- if (existsSync5(join5(dir, WORKSPACE_ROOT_MARKER))) return dir;
358
+ if (existsSync5(join6(dir, WORKSPACE_ROOT_MARKER))) return dir;
308
359
  const parent = dirname2(dir);
309
360
  if (parent === dir) return void 0;
310
361
  dir = parent;
@@ -317,9 +368,9 @@ function declaredNativeTs(pkg) {
317
368
  return version || void 0;
318
369
  }
319
370
  function ensureI18nextSingleton(root, dryRun) {
320
- const pkgPath = join5(root, "package.json");
371
+ const pkgPath = join6(root, "package.json");
321
372
  if (!existsSync5(pkgPath)) return void 0;
322
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
373
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
323
374
  const want = declaredNativeTs(pkg);
324
375
  if (!want) return void 0;
325
376
  const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
@@ -332,10 +383,10 @@ function ensureI18nextSingleton(root, dryRun) {
332
383
  return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
333
384
  }
334
385
  function ensureReleaseAgeAllowList(root, dryRun) {
335
- const yamlPath = join5(root, WORKSPACE_YAML);
386
+ const yamlPath = join6(root, WORKSPACE_YAML);
336
387
  if (!existsSync5(yamlPath)) return void 0;
337
388
  const own = readOwnPackage().name;
338
- const lines = readFileSync3(yamlPath, "utf8").split("\n");
389
+ const lines = readFileSync4(yamlPath, "utf8").split("\n");
339
390
  const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
340
391
  if (keyIdx === -1) return void 0;
341
392
  let lastItemIdx = keyIdx;
@@ -370,11 +421,11 @@ var ROOT_PRETTIER_CONFIGS = [
370
421
  function ensureFormatterExclusion(root, moduleDir, dryRun) {
371
422
  const rel = relative(root, moduleDir).replaceAll("\\", "/");
372
423
  if (rel === "" || rel.startsWith("..")) return void 0;
373
- const ignorePath = join5(root, PRETTIER_IGNORE);
374
- const hasPrettier = existsSync5(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync5(join5(root, name)));
424
+ const ignorePath = join6(root, PRETTIER_IGNORE);
425
+ const hasPrettier = existsSync5(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync5(join6(root, name)));
375
426
  if (!hasPrettier) return void 0;
376
427
  const pattern = `/${rel}/`;
377
- const existing = existsSync5(ignorePath) ? readFileSync3(ignorePath, "utf8") : "";
428
+ const existing = existsSync5(ignorePath) ? readFileSync4(ignorePath, "utf8") : "";
378
429
  const lines = existing.split("\n");
379
430
  if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
380
431
  return void 0;
@@ -407,16 +458,50 @@ function ensureWorkspacePrep(opts) {
407
458
  opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
408
459
  ].filter((message) => message !== void 0);
409
460
  }
461
+ function inspectWorkspacePrep(root) {
462
+ const entries = [];
463
+ let pkg = {};
464
+ const pkgPath = join6(root, "package.json");
465
+ if (existsSync5(pkgPath)) {
466
+ try {
467
+ pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
468
+ } catch {
469
+ pkg = {};
470
+ }
471
+ }
472
+ const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
473
+ if (pinned !== void 0) {
474
+ const stillForks = declaredNativeTs(pkg);
475
+ entries.push({
476
+ rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
477
+ reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
478
+ });
479
+ }
480
+ const own = readOwnPackage().name;
481
+ const yamlPath = join6(root, WORKSPACE_YAML);
482
+ if (existsSync5(yamlPath)) {
483
+ const yaml = readFileSync4(yamlPath, "utf8");
484
+ const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
485
+ if (listed) {
486
+ const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
487
+ entries.push({
488
+ rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
489
+ reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
490
+ });
491
+ }
492
+ }
493
+ return entries;
494
+ }
410
495
 
411
496
  // src/shared/resolve-bin.ts
412
497
  import { existsSync as existsSync6 } from "fs";
413
498
  import { createRequire } from "module";
414
- import { delimiter, dirname as dirname3, join as join6 } from "path";
499
+ import { delimiter, dirname as dirname3, join as join7 } from "path";
415
500
  var require2 = createRequire(import.meta.url);
416
501
  function resolveBin(fromDir, name) {
417
502
  let dir = fromDir;
418
503
  for (; ; ) {
419
- const candidate = join6(dir, "node_modules", ".bin", name);
504
+ const candidate = join7(dir, "node_modules", ".bin", name);
420
505
  if (existsSync6(candidate)) return candidate;
421
506
  const parent = dirname3(dir);
422
507
  if (parent === dir) return void 0;
@@ -429,7 +514,7 @@ function binFromOwnInstall(packageName, binName) {
429
514
  const bin = require2(manifest).bin;
430
515
  const relative3 = typeof bin === "string" ? bin : bin?.[binName];
431
516
  if (!relative3) return void 0;
432
- const executable = join6(dirname3(manifest), relative3);
517
+ const executable = join7(dirname3(manifest), relative3);
433
518
  return existsSync6(executable) ? executable : void 0;
434
519
  } catch {
435
520
  return void 0;
@@ -463,6 +548,7 @@ var PERMITTED_LOCAL_KEYS = [
463
548
  "experimentalOperatorPosition"
464
549
  ];
465
550
  var ALWAYS_LOCAL_KEYS = ["ignorePatterns", "overrides"];
551
+ var FORMAT_DEFAULT_IGNORES = ["**/*.toml"];
466
552
  var PRETTIER_CONFIG_FILES = [
467
553
  ".prettierrc",
468
554
  ".prettierrc.json",
@@ -556,8 +642,8 @@ function writesWhenRewritten(name, command) {
556
642
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
557
643
 
558
644
  // src/roles/format/inherited-ignores.ts
559
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
560
- import { join as join7 } from "path";
645
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
646
+ import { join as join8 } from "path";
561
647
  var ROOT_IGNORE_FILE = ".prettierignore";
562
648
  function isPattern(line) {
563
649
  const trimmed = line.trim();
@@ -572,11 +658,11 @@ function toModulePattern(pattern) {
572
658
  }
573
659
  function inheritedIgnorePatterns(workspaceRoot) {
574
660
  if (!workspaceRoot) return [];
575
- const path = join7(workspaceRoot, ROOT_IGNORE_FILE);
661
+ const path = join8(workspaceRoot, ROOT_IGNORE_FILE);
576
662
  if (!existsSync7(path)) return [];
577
663
  let contents;
578
664
  try {
579
- contents = readFileSync4(path, "utf8");
665
+ contents = readFileSync5(path, "utf8");
580
666
  } catch {
581
667
  return [];
582
668
  }
@@ -663,25 +749,13 @@ function formatPresetFor(preset) {
663
749
  }
664
750
 
665
751
  // src/roles/format/prettier-config.ts
666
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
667
- import { join as join9 } from "path";
668
-
669
- // src/shared/jsonc.ts
670
- import { parse, printParseErrorCode } from "jsonc-parser";
671
- function parseJsonc(text, source = "config") {
672
- const errors = [];
673
- const value = parse(text, errors, { allowTrailingComma: true });
674
- if (errors.length > 0) {
675
- const details = errors.map((error) => printParseErrorCode(error.error)).join(", ");
676
- throw new Error(`${source}: malformed JSONC (${details}).`);
677
- }
678
- return value;
679
- }
752
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
753
+ import { join as join10 } from "path";
680
754
 
681
755
  // src/roles/format/resolve-oxfmt.ts
682
- import { readFileSync as readFileSync5 } from "fs";
756
+ import { readFileSync as readFileSync6 } from "fs";
683
757
  import { createRequire as createRequire2 } from "module";
684
- import { dirname as dirname4, join as join8 } from "path";
758
+ import { dirname as dirname4, join as join9 } from "path";
685
759
  function resolveOxfmt(cwd) {
686
760
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
687
761
  }
@@ -710,8 +784,8 @@ function configSchema() {
710
784
  function readConfigSchema() {
711
785
  try {
712
786
  const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
713
- const schemaPath = join8(dirname4(manifest), "configuration_schema.json");
714
- return JSON.parse(readFileSync5(schemaPath, "utf8"));
787
+ const schemaPath = join9(dirname4(manifest), "configuration_schema.json");
788
+ return JSON.parse(readFileSync6(schemaPath, "utf8"));
715
789
  } catch {
716
790
  return void 0;
717
791
  }
@@ -742,14 +816,14 @@ function toOxfmtOverrides(value) {
742
816
  return { overrides, unresolved };
743
817
  }
744
818
  function readPrettierSettings(cwd) {
745
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync8(join9(cwd, name)));
819
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync8(join10(cwd, name)));
746
820
  if (!file) return { options: {}, unresolved: [] };
747
821
  if (/\.(js|cjs|mjs)$/.test(file)) {
748
822
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
749
823
  }
750
824
  let parsed;
751
825
  try {
752
- parsed = parseJsonc(readFileSync6(join9(cwd, file), "utf8"), file);
826
+ parsed = parseJsonc(readFileSync7(join10(cwd, file), "utf8"), file);
753
827
  } catch {
754
828
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
755
829
  }
@@ -779,8 +853,8 @@ function readPrettierSettings(cwd) {
779
853
  }
780
854
 
781
855
  // src/roles/format/read-adoption.ts
782
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
783
- import { join as join10 } from "path";
856
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
857
+ import { join as join11 } from "path";
784
858
  var NOT_ADOPTED = (configFile, unreadable = null) => ({
785
859
  configFile,
786
860
  preset: null,
@@ -796,11 +870,11 @@ function sameValue(a, b) {
796
870
  return JSON.stringify(a) === JSON.stringify(b);
797
871
  }
798
872
  function readFormatAdoption(cwd) {
799
- const path = join10(cwd, FORMAT_CONFIG_FILE);
873
+ const path = join11(cwd, FORMAT_CONFIG_FILE);
800
874
  if (!existsSync9(path)) return NOT_ADOPTED(null);
801
875
  let parsed;
802
876
  try {
803
- parsed = parseJsonc(readFileSync7(path, "utf8"), FORMAT_CONFIG_FILE);
877
+ parsed = parseJsonc(readFileSync8(path, "utf8"), FORMAT_CONFIG_FILE);
804
878
  } catch (error) {
805
879
  const reason = error instanceof Error ? error.message : String(error);
806
880
  return NOT_ADOPTED(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
@@ -887,7 +961,9 @@ var OxfmtAdapter = class extends BaseAdapter {
887
961
  const localKeys = Object.keys(local).filter((key) => !ALWAYS_LOCAL_KEYS.includes(key)).sort();
888
962
  const inherited = inheritedIgnorePatterns(findWorkspaceRoot(context.cwd));
889
963
  const declaredIgnores = Array.isArray(local.ignorePatterns) ? local.ignorePatterns.filter((v) => typeof v === "string") : [];
890
- const ignorePatterns = [.../* @__PURE__ */ new Set([...declaredIgnores, ...inherited])].sort();
964
+ const ignorePatterns = [
965
+ .../* @__PURE__ */ new Set([...declaredIgnores, ...inherited, ...FORMAT_DEFAULT_IGNORES])
966
+ ].sort();
891
967
  if (ignorePatterns.length > 0) local.ignorePatterns = ignorePatterns;
892
968
  const unchanged = existing.adopted && existing.conformant;
893
969
  const version = unchanged && existing.presetVersion ? existing.presetVersion : own.version;
@@ -907,7 +983,7 @@ var OxfmtAdapter = class extends BaseAdapter {
907
983
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
908
984
  ];
909
985
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
910
- (name) => existsSync10(join11(context.cwd, name))
986
+ (name) => existsSync10(join12(context.cwd, name))
911
987
  );
912
988
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
913
989
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -1143,7 +1219,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1143
1219
  modulePrettierDependencies(cwd) {
1144
1220
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
1145
1221
  try {
1146
- const manifest = JSON.parse(readFileSync8(join11(cwd, "package.json"), "utf8"));
1222
+ const manifest = JSON.parse(readFileSync9(join12(cwd, "package.json"), "utf8"));
1147
1223
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
1148
1224
  } catch {
1149
1225
  return [];
@@ -1166,7 +1242,9 @@ var OxfmtAdapter = class extends BaseAdapter {
1166
1242
  const ownCommand = selfCommand(cwd, "--run --format");
1167
1243
  const compose = (existing, keepFix) => composeFormatScript(existing, { keepFix, command: ownCommand });
1168
1244
  const rewritten = {
1169
- [FORMAT_SCRIPT_NAME]: compose(scripts[FORMAT_SCRIPT_NAME], false),
1245
+ // `existingCommand` for the role's own script: the command can live in an
1246
+ // `nx:run-commands` target that adoption deletes, taking anything else it ran with it.
1247
+ [FORMAT_SCRIPT_NAME]: compose(existingCommand(cwd, FORMAT_SCRIPT_NAME), false),
1170
1248
  [`${FORMAT_SCRIPT_NAME}:fix`]: compose(scripts[`${FORMAT_SCRIPT_NAME}:fix`], true)
1171
1249
  };
1172
1250
  for (const [name, command] of Object.entries(scripts)) {
@@ -1223,8 +1301,8 @@ function registerFormat() {
1223
1301
 
1224
1302
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1225
1303
  import { spawnSync as spawnSync2 } from "child_process";
1226
- import { existsSync as existsSync16, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync2 } from "fs";
1227
- import { join as join18 } from "path";
1304
+ import { existsSync as existsSync17, readFileSync as readFileSync16, rmSync, writeFileSync as writeFileSync2 } from "fs";
1305
+ import { join as join20 } from "path";
1228
1306
 
1229
1307
  // src/core/config/deferred-rules.ts
1230
1308
  function deferredRuleNames(rules) {
@@ -1238,6 +1316,14 @@ var PRESET_DIR = "./node_modules/@hublo/sentinel/oxlint";
1238
1316
  function presetPath(preset) {
1239
1317
  return `${PRESET_DIR}/${preset}.json`;
1240
1318
  }
1319
+ function isSentinelPreset(entry) {
1320
+ return entry.includes("@hublo/sentinel/oxlint/");
1321
+ }
1322
+ function extendsWithPreset(current, variant) {
1323
+ const own = presetPath(variant);
1324
+ const others = current.filter((entry) => entry !== own && !isSentinelPreset(entry));
1325
+ return [own, ...new Set(others)];
1326
+ }
1241
1327
  function presetVariant(preset, cwd) {
1242
1328
  const normalised = cwd.replaceAll("\\", "/");
1243
1329
  return preset === "react" && /(^|\/)libs\//.test(normalised) ? "react-lib" : preset;
@@ -1257,7 +1343,7 @@ var ESLINT_CONFIG_FILES = [
1257
1343
  "eslint.config.ts"
1258
1344
  ];
1259
1345
  function lintTarget() {
1260
- return { cache: true, inputs: ["default", `{projectRoot}/${LINT_CONFIG_FILE}`] };
1346
+ return { cache: true, inputs: ["default", "^default", `{projectRoot}/${LINT_CONFIG_FILE}`] };
1261
1347
  }
1262
1348
 
1263
1349
  // src/roles/lint/presets/base.json
@@ -1601,8 +1687,8 @@ var nest_default = {
1601
1687
  },
1602
1688
  "typescript/return-await": {
1603
1689
  severity: "warn",
1604
- reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. The `never` option matches what no-return-await meant, so this is like-for-like, but it now runs over every file and finds real violations (9 on bff-admin, 5 on host-admin, measured with --type-aware). Held at warn so adoption does not turn a green module red, and raised to error once they are fixed. Switching to the stricter `in-try-catch`, which typescript-eslint recommends because `return await` inside try IS needed for correct error handling, is a separate decision with its own count.",
1605
- options: ["never"]
1690
+ reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. `in-try-catch` rather than `never`, and the planning adoption is what settled it. `never` forces `return await f()` to become `const x = await f(); return x` even inside a `try`, where the await is REQUIRED for the catch to fire: it produced 10 such edits across 15 modules, every one of them inside a try/catch, and every one of them a rewrite that made the code longer without making it safer. Worse, the obvious reading of the rule is to delete the `await`, which silently stops the catch from running. `in-try-catch` is what typescript-eslint recommends and flags none of those 10. Held at warn so adoption does not turn a green module red, and raised to error once the remaining findings are fixed.",
1691
+ options: ["in-try-catch"]
1606
1692
  },
1607
1693
  "typescript/switch-exhaustiveness-check": [
1608
1694
  "error",
@@ -1993,8 +2079,8 @@ var react_lib_default = {
1993
2079
  },
1994
2080
  "typescript/return-await": {
1995
2081
  severity: "warn",
1996
- reason: "replaces the deprecated core `no-return-await`. The `never` option is what makes it mean the same thing. Held at warn to match the app tier, where running it over every file found real violations the old rule never reported.",
1997
- options: ["never"]
2082
+ reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. `in-try-catch` rather than `never`, and the planning adoption is what settled it. `never` forces `return await f()` to become `const x = await f(); return x` even inside a `try`, where the await is REQUIRED for the catch to fire: it produced 10 such edits across 15 modules, every one of them inside a try/catch, and every one of them a rewrite that made the code longer without making it safer. Worse, the obvious reading of the rule is to delete the `await`, which silently stops the catch from running. `in-try-catch` is what typescript-eslint recommends and flags none of those 10. Held at warn so adoption does not turn a green module red, and raised to error once the remaining findings are fixed.",
2083
+ options: ["in-try-catch"]
1998
2084
  },
1999
2085
  "use-isnan": [
2000
2086
  "error",
@@ -2567,8 +2653,8 @@ var react_default = {
2567
2653
  "typescript/restrict-template-expressions": "error",
2568
2654
  "typescript/return-await": {
2569
2655
  severity: "warn",
2570
- reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. The `never` option matches what no-return-await meant, so this is like-for-like, but it now runs over every file and finds real violations (9 on bff-admin, 5 on host-admin, measured with --type-aware). Held at warn so adoption does not turn a green module red, and raised to error once they are fixed. Switching to the stricter `in-try-catch`, which typescript-eslint recommends because `return await` inside try IS needed for correct error handling, is a separate decision with its own count.",
2571
- options: ["never"]
2656
+ reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. `in-try-catch` rather than `never`, and the planning adoption is what settled it. `never` forces `return await f()` to become `const x = await f(); return x` even inside a `try`, where the await is REQUIRED for the catch to fire: it produced 10 such edits across 15 modules, every one of them inside a try/catch, and every one of them a rewrite that made the code longer without making it safer. Worse, the obvious reading of the rule is to delete the `await`, which silently stops the catch from running. `in-try-catch` is what typescript-eslint recommends and flags none of those 10. Held at warn so adoption does not turn a green module red, and raised to error once the remaining findings are fixed.",
2657
+ options: ["in-try-catch"]
2572
2658
  },
2573
2659
  "typescript/switch-exhaustiveness-check": {
2574
2660
  severity: "warn",
@@ -3029,6 +3115,10 @@ function downgradedRulesFor(preset) {
3029
3115
  return downgradedFor(preset);
3030
3116
  }
3031
3117
 
3118
+ // src/roles/lint/extra-layers.ts
3119
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3120
+ import { isAbsolute as isAbsolute2, join as join13, resolve as resolve3 } from "path";
3121
+
3032
3122
  // src/roles/lint/module-baseline.ts
3033
3123
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
3034
3124
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
@@ -3037,10 +3127,7 @@ function holdsFrom(errorCounts) {
3037
3127
  return [...errorCounts].map(([rule, count]) => ({ rule, count })).sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule));
3038
3128
  }
3039
3129
  function renderBaseline(holds, version) {
3040
- const entries = holds.map(
3041
- ({ rule, count }) => ` // ${count} violation${count === 1 ? "" : "s"} when this module adopted
3042
- ${JSON.stringify(rule)}: "warn"`
3043
- ).join(",\n");
3130
+ const entries = holds.map(({ rule }) => ` ${JSON.stringify(rule)}: "warn"`).join(",\n");
3044
3131
  return [
3045
3132
  "{",
3046
3133
  ` // Written by @hublo/sentinel@${version}. Do not edit by hand: \`sentinel --init --lint\``,
@@ -3048,9 +3135,15 @@ function renderBaseline(holds, version) {
3048
3135
  " // and returns to `error` on its own.",
3049
3136
  " //",
3050
3137
  " // These rules are held at `warn` because this module ALREADY violated them when it",
3051
- " // adopted. They still run and still report; they just cannot fail the build for code",
3052
- " // that was there before the migration. Fix them and re-run `--init --lint` to get the",
3053
- " // preset severity back.",
3138
+ " // adopted. They still run and still report, so the build cannot fail on them.",
3139
+ " //",
3140
+ " // The hold is per RULE, not per occurrence: oxlint cannot freeze a specific list of",
3141
+ " // violations, so a NEW violation of one of these rules is covered too, and will only",
3142
+ " // warn. That is the cost of adopting without a red build, and the reason to clear this",
3143
+ " // file rather than live with it.",
3144
+ " //",
3145
+ " // `sentinel --inspect --lint` counts what is left. Fix them and re-run `--init --lint`",
3146
+ " // to get the preset severity back.",
3054
3147
  ' "rules": {',
3055
3148
  entries,
3056
3149
  " }",
@@ -3070,6 +3163,44 @@ function describeHolds(holds) {
3070
3163
  return `held ${holds.length} rule(s) at \`warn\` for this module, covering ${total} violation(s) that were already there: ${listed}${rest}. They are written to ${LINT_BASELINE_FILE}, they still report, and they return to \`error\` once fixed and re-initialized`;
3071
3164
  }
3072
3165
 
3166
+ // src/roles/lint/extra-layers.ts
3167
+ function isSentinelPreset2(entry) {
3168
+ return entry.includes("@hublo/sentinel/oxlint/");
3169
+ }
3170
+ function ruleCount(cwd, specifier) {
3171
+ const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
3172
+ if (!existsSync11(path)) return void 0;
3173
+ try {
3174
+ const parsed = parseJsonc(
3175
+ readFileSync10(path, "utf8"),
3176
+ specifier
3177
+ );
3178
+ return Object.keys(parsed.rules ?? {}).length;
3179
+ } catch {
3180
+ return void 0;
3181
+ }
3182
+ }
3183
+ function extraLayers(cwd, extendsList) {
3184
+ return extendsList.filter((entry) => !isSentinelPreset2(entry) && entry !== LINT_BASELINE_SPECIFIER).map((entry) => {
3185
+ const count = ruleCount(cwd, entry);
3186
+ return {
3187
+ rule: entry,
3188
+ reason: count === void 0 ? "this module extends it, but it could not be read from here" : `${count} rule(s) on top of the preset, applied after it`
3189
+ };
3190
+ });
3191
+ }
3192
+ function committedExtendsList(cwd, configFile) {
3193
+ try {
3194
+ const parsed = parseJsonc(
3195
+ readFileSync10(join13(cwd, configFile), "utf8"),
3196
+ configFile
3197
+ );
3198
+ return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
3199
+ } catch {
3200
+ return [];
3201
+ }
3202
+ }
3203
+
3073
3204
  // src/roles/lint/parse-diagnostics.ts
3074
3205
  function ruleId(code) {
3075
3206
  const inner = /\(([^)]+)\)/.exec(code);
@@ -3120,8 +3251,8 @@ function errorCountsByConfigRule(stdout) {
3120
3251
  }
3121
3252
 
3122
3253
  // src/core/config/read-adoption.ts
3123
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
3124
- import { join as join13 } from "path";
3254
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
3255
+ import { join as join15 } from "path";
3125
3256
 
3126
3257
  // src/core/config/owned-keys.ts
3127
3258
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -3136,12 +3267,12 @@ function localOnlyKeys(config, permitted, presetSets) {
3136
3267
  }
3137
3268
 
3138
3269
  // src/core/config/resolve-config-target.ts
3139
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3140
- import { join as join12 } from "path";
3270
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3271
+ import { join as join14 } from "path";
3141
3272
  function readExtends(absolutePath) {
3142
3273
  let parsed;
3143
3274
  try {
3144
- parsed = parseJsonc(readFileSync9(absolutePath, "utf8"), absolutePath);
3275
+ parsed = parseJsonc(readFileSync11(absolutePath, "utf8"), absolutePath);
3145
3276
  } catch {
3146
3277
  return [];
3147
3278
  }
@@ -3155,8 +3286,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
3155
3286
  let existing;
3156
3287
  let existingExtendsSomething = false;
3157
3288
  for (const candidate of candidates) {
3158
- const absolutePath = join12(moduleDir, candidate);
3159
- if (!existsSync11(absolutePath)) continue;
3289
+ const absolutePath = join14(moduleDir, candidate);
3290
+ if (!existsSync12(absolutePath)) continue;
3160
3291
  const chain = readExtends(absolutePath);
3161
3292
  if (existing === void 0) {
3162
3293
  existing = candidate;
@@ -3189,12 +3320,12 @@ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
3189
3320
  });
3190
3321
  function readAdoption(cwd, options) {
3191
3322
  const target = resolveConfigTarget(cwd, options);
3192
- if (target.reason === "none" || !existsSync12(join13(cwd, target.path))) {
3323
+ if (target.reason === "none" || !existsSync13(join15(cwd, target.path))) {
3193
3324
  return NOT_ADOPTED2(target.reason === "none" ? null : target.path);
3194
3325
  }
3195
3326
  let parsed;
3196
3327
  try {
3197
- parsed = parseJsonc(readFileSync10(join13(cwd, target.path), "utf8"), target.path);
3328
+ parsed = parseJsonc(readFileSync12(join15(cwd, target.path), "utf8"), target.path);
3198
3329
  } catch (error) {
3199
3330
  const reason = error instanceof Error ? error.message : String(error);
3200
3331
  return NOT_ADOPTED2(target.path, `${target.path} could not be parsed (${reason})`);
@@ -3228,9 +3359,9 @@ function readLintAdoption(cwd) {
3228
3359
  }
3229
3360
 
3230
3361
  // src/roles/lint/resolve-oxlint.ts
3231
- import { existsSync as existsSync13 } from "fs";
3362
+ import { existsSync as existsSync14 } from "fs";
3232
3363
  import { createRequire as createRequire3 } from "module";
3233
- import { delimiter as delimiter2, dirname as dirname5, join as join14 } from "path";
3364
+ import { delimiter as delimiter2, dirname as dirname5, join as join16 } from "path";
3234
3365
  import { fileURLToPath as fileURLToPath2 } from "url";
3235
3366
  var PACKAGE_OF = {
3236
3367
  oxlint: "oxlint",
@@ -3255,30 +3386,30 @@ function tsgolintShim(cwd) {
3255
3386
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
3256
3387
  try {
3257
3388
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
3258
- candidates.push(join14(packageDir, "node_modules", ".bin", "tsgolint"));
3259
- candidates.push(join14(packageDir, "..", ".bin", "tsgolint"));
3389
+ candidates.push(join16(packageDir, "node_modules", ".bin", "tsgolint"));
3390
+ candidates.push(join16(packageDir, "..", ".bin", "tsgolint"));
3260
3391
  } catch {
3261
3392
  }
3262
3393
  }
3263
3394
  try {
3264
3395
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
3265
- candidates.push(join14(ownRoot, "node_modules", ".bin", "tsgolint"));
3396
+ candidates.push(join16(ownRoot, "node_modules", ".bin", "tsgolint"));
3266
3397
  } catch {
3267
3398
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
3268
3399
  }
3269
- return candidates.find((candidate) => candidate !== "" && existsSync13(candidate));
3400
+ return candidates.find((candidate) => candidate !== "" && existsSync14(candidate));
3270
3401
  }
3271
3402
  function oxlintSearchPath(cwd) {
3272
3403
  return binSearchPath(cwd);
3273
3404
  }
3274
3405
 
3275
3406
  // src/roles/lint/adapters/oxlint/plan.ts
3276
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3277
- import { join as join17 } from "path";
3407
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
3408
+ import { join as join19 } from "path";
3278
3409
 
3279
3410
  // src/roles/lint/eslint-ignores.ts
3280
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
3281
- import { join as join15 } from "path";
3411
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3412
+ import { join as join17 } from "path";
3282
3413
  var IGNORE_BLOCKS = [/\bignores\s*:\s*\[([^\]]*)\]/g, /\bglobalIgnores\s*\(\s*\[([^\]]*)\]/g];
3283
3414
  var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
3284
3415
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
@@ -3291,11 +3422,11 @@ function readRootEslintIgnores(root) {
3291
3422
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
3292
3423
  }
3293
3424
  function readEslintIgnores(cwd) {
3294
- const config = ESLINT_CONFIG_FILES.map((name) => join15(cwd, name)).find((path) => existsSync14(path));
3425
+ const config = ESLINT_CONFIG_FILES.map((name) => join17(cwd, name)).find((path) => existsSync15(path));
3295
3426
  if (!config) return { patterns: [], unresolved: [] };
3296
3427
  let source;
3297
3428
  try {
3298
- source = readFileSync11(config, "utf8");
3429
+ source = readFileSync13(config, "utf8");
3299
3430
  } catch {
3300
3431
  return { patterns: [], unresolved: [] };
3301
3432
  }
@@ -3349,8 +3480,8 @@ function lintPresetFor(preset) {
3349
3480
  }
3350
3481
 
3351
3482
  // src/roles/lint/rename-suppressions.ts
3352
- import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync } from "fs";
3353
- import { join as join16, relative as relative2 } from "path";
3483
+ import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync } from "fs";
3484
+ import { join as join18, relative as relative2 } from "path";
3354
3485
  var SOURCE_EXTENSIONS = [
3355
3486
  ".ts",
3356
3487
  ".tsx",
@@ -3397,7 +3528,7 @@ function* sourceFiles(dir) {
3397
3528
  return;
3398
3529
  }
3399
3530
  for (const entry of entries) {
3400
- const full = join16(dir, entry);
3531
+ const full = join18(dir, entry);
3401
3532
  let isDirectory;
3402
3533
  try {
3403
3534
  isDirectory = statSync(full).isDirectory();
@@ -3417,7 +3548,7 @@ function findSuppressionRenames(cwd, renames) {
3417
3548
  for (const file of sourceFiles(cwd)) {
3418
3549
  let content;
3419
3550
  try {
3420
- content = readFileSync12(file, "utf8");
3551
+ content = readFileSync14(file, "utf8");
3421
3552
  } catch {
3422
3553
  continue;
3423
3554
  }
@@ -3457,7 +3588,17 @@ var DEFAULT_IGNORE_PATTERNS = [
3457
3588
  "**/dist/**",
3458
3589
  "**/node_modules/**",
3459
3590
  "**/coverage/**",
3460
- "**/*.mock.ts",
3591
+ // `**/*.mock.ts` used to sit here and has been removed. It was the only entry in this list
3592
+ // with no reason written next to it, and it does not meet the bar the paragraph above sets:
3593
+ // a mock is not build output, not vendored and not generated. It is hand-written TypeScript
3594
+ // that ships with the tests, and lint rules have as much to say about it as about any other
3595
+ // source file. Ignoring it quietly narrowed what an adopted module checks — career lost
3596
+ // `src/test/router.mock.ts`, which its previous ESLint config did lint.
3597
+ //
3598
+ // Measured before removing, because this is the one change that can turn a green module red:
3599
+ // career has 1 such file and it reports nothing; the 15 planning modules have none. 222 exist
3600
+ // repo-wide, and modules adopting later meet them through the baseline, which is exactly what
3601
+ // the baseline is for.
3461
3602
  // Tool configuration is not source. A repo's shared ESLint config ignores these, and every
3462
3603
  // module inherits that; adoption replaces the config chain, so without them the first thing
3463
3604
  // an adopted module lints is its own jest setup file. Found on libs/front/components, where
@@ -3520,7 +3661,7 @@ function plan(context) {
3520
3661
  ].filter((pattern) => !defaults.has(bare(pattern)));
3521
3662
  const carried = [.../* @__PURE__ */ new Set([...DEFAULT_IGNORE_PATTERNS, ...moduleOwn])];
3522
3663
  const stub = {
3523
- extends: [presetPath(variant)],
3664
+ extends: extendsWithPreset(committedExtends(context.cwd), variant),
3524
3665
  ...carried.length > 0 ? { ignorePatterns: carried } : {}
3525
3666
  };
3526
3667
  const operations = [
@@ -3535,7 +3676,7 @@ function plan(context) {
3535
3676
  keys: removableDeps
3536
3677
  });
3537
3678
  }
3538
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync15(join17(context.cwd, name)));
3679
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync16(join19(context.cwd, name)));
3539
3680
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
3540
3681
  operations.push(
3541
3682
  ...nxTargetOperations({
@@ -3611,7 +3752,10 @@ function plan(context) {
3611
3752
  function lintScripts(cwd) {
3612
3753
  const scripts = moduleScripts(cwd);
3613
3754
  const rewritten = {
3614
- [LINT_SCRIPT_NAME]: composeLintScript(scripts[LINT_SCRIPT_NAME], {
3755
+ // `existingCommand`, not `scripts[...]`: the command can live in an `nx:run-commands`
3756
+ // target instead of a script, and adoption deletes that target. Composing against only
3757
+ // the script threw away whatever else the target ran.
3758
+ [LINT_SCRIPT_NAME]: composeLintScript(existingCommand(cwd, LINT_SCRIPT_NAME), {
3615
3759
  command: selfCommand(cwd, "--run --lint")
3616
3760
  })
3617
3761
  };
@@ -3627,10 +3771,21 @@ function lintScripts(cwd) {
3627
3771
  }
3628
3772
  return rewritten;
3629
3773
  }
3774
+ function committedExtends(cwd) {
3775
+ try {
3776
+ const parsed = parseJsonc(
3777
+ readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
3778
+ LINT_CONFIG_FILE
3779
+ );
3780
+ return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
3781
+ } catch {
3782
+ return [];
3783
+ }
3784
+ }
3630
3785
  function committedIgnorePatterns(cwd) {
3631
3786
  try {
3632
3787
  const parsed = parseJsonc(
3633
- readFileSync13(join17(cwd, LINT_CONFIG_FILE), "utf8"),
3788
+ readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
3634
3789
  LINT_CONFIG_FILE
3635
3790
  );
3636
3791
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -3642,7 +3797,7 @@ function moduleEslintDependencies(cwd) {
3642
3797
  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/");
3643
3798
  let manifest;
3644
3799
  try {
3645
- manifest = JSON.parse(readFileSync13(join17(cwd, "package.json"), "utf8"));
3800
+ manifest = JSON.parse(readFileSync15(join19(cwd, "package.json"), "utf8"));
3646
3801
  } catch {
3647
3802
  return [];
3648
3803
  }
@@ -3761,20 +3916,20 @@ var OxlintAdapter = class extends BaseAdapter {
3761
3916
  * build the developer can see, rather than a silent half-adoption they cannot.
3762
3917
  */
3763
3918
  writeModuleBaseline(ctx, oxlint, env) {
3764
- const configPath = join18(ctx.cwd, LINT_CONFIG_FILE);
3765
- const baselinePath = join18(ctx.cwd, LINT_BASELINE_FILE);
3766
- if (!existsSync16(configPath)) return;
3919
+ const configPath = join20(ctx.cwd, LINT_CONFIG_FILE);
3920
+ const baselinePath = join20(ctx.cwd, LINT_BASELINE_FILE);
3921
+ if (!existsSync17(configPath)) return;
3767
3922
  let config;
3768
3923
  try {
3769
3924
  config = parseJsonc(
3770
- readFileSync14(configPath, "utf8"),
3925
+ readFileSync16(configPath, "utf8"),
3771
3926
  LINT_CONFIG_FILE
3772
3927
  );
3773
3928
  } catch {
3774
3929
  return;
3775
3930
  }
3776
3931
  const current = Array.isArray(config.extends) ? config.extends : [];
3777
- const measurePath = join18(ctx.cwd, LINT_MEASURE_FILE);
3932
+ const measurePath = join20(ctx.cwd, LINT_MEASURE_FILE);
3778
3933
  let measured;
3779
3934
  try {
3780
3935
  writeFileSync2(
@@ -3822,7 +3977,7 @@ var OxlintAdapter = class extends BaseAdapter {
3822
3977
  }
3823
3978
  }
3824
3979
  async run(ctx) {
3825
- if (!existsSync16(join18(ctx.cwd, LINT_CONFIG_FILE))) {
3980
+ if (!existsSync17(join20(ctx.cwd, LINT_CONFIG_FILE))) {
3826
3981
  process.stderr.write(
3827
3982
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
3828
3983
  `
@@ -3906,7 +4061,12 @@ var OxlintAdapter = class extends BaseAdapter {
3906
4061
  downgraded: downgradedRulesFor(presetVariant(ctx.preset, ctx.cwd)).map((entry) => ({
3907
4062
  rule: entry.rule,
3908
4063
  reason: entry.reason
3909
- }))
4064
+ })),
4065
+ // What this module enforces BEYOND the preset. Not drift, and not covered by anything
4066
+ // else here: the stub still holds only `extends` and `ignorePatterns`, so a module with
4067
+ // a team layer reports `conformant=true drift=[]` and used to say nothing at all about
4068
+ // the extra rules it runs. See `extra-layers` for why that state is legitimate.
4069
+ layers: extraLayers(ctx.cwd, committedExtendsList(ctx.cwd, LINT_CONFIG_FILE))
3910
4070
  };
3911
4071
  }
3912
4072
  /** How many rules the extended preset enforces, read from the preset on disk. */
@@ -3937,7 +4097,7 @@ var OxlintAdapter = class extends BaseAdapter {
3937
4097
  let stub;
3938
4098
  try {
3939
4099
  stub = parseJsonc(
3940
- readFileSync14(join18(cwd, LINT_CONFIG_FILE), "utf8"),
4100
+ readFileSync16(join20(cwd, LINT_CONFIG_FILE), "utf8"),
3941
4101
  LINT_CONFIG_FILE
3942
4102
  );
3943
4103
  } catch {
@@ -3947,7 +4107,7 @@ var OxlintAdapter = class extends BaseAdapter {
3947
4107
  for (const entry of stub.extends ?? []) {
3948
4108
  try {
3949
4109
  const preset = parseJsonc(
3950
- readFileSync14(join18(cwd, entry), "utf8"),
4110
+ readFileSync16(join20(cwd, entry), "utf8"),
3951
4111
  entry
3952
4112
  );
3953
4113
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -4017,14 +4177,14 @@ var OxlintAdapter = class extends BaseAdapter {
4017
4177
  let parsed;
4018
4178
  try {
4019
4179
  parsed = parseJsonc(
4020
- readFileSync14(join18(cwd, LINT_CONFIG_FILE), "utf8"),
4180
+ readFileSync16(join20(cwd, LINT_CONFIG_FILE), "utf8"),
4021
4181
  LINT_CONFIG_FILE
4022
4182
  );
4023
4183
  } catch {
4024
4184
  return void 0;
4025
4185
  }
4026
4186
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
4027
- return targets.find((target) => !existsSync16(join18(cwd, target)));
4187
+ return targets.find((target) => !existsSync17(join20(cwd, target)));
4028
4188
  }
4029
4189
  /** Announce what is not enforced, so reduced coverage is never silent. */
4030
4190
  announceDisabled(preset) {
@@ -4056,9 +4216,9 @@ function registerLint() {
4056
4216
 
4057
4217
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
4058
4218
  import { spawnSync as spawnSync3 } from "child_process";
4059
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
4219
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
4060
4220
  import { createRequire as createRequire4 } from "module";
4061
- import { join as join20 } from "path";
4221
+ import { join as join22 } from "path";
4062
4222
 
4063
4223
  // src/roles/typescript/presets/base.json
4064
4224
  var base_default3 = {
@@ -4239,8 +4399,8 @@ function readTsconfigAdoption(cwd) {
4239
4399
  }
4240
4400
 
4241
4401
  // src/roles/typescript/adapters/tsc/plan.ts
4242
- import { readFileSync as readFileSync15 } from "fs";
4243
- import { join as join19 } from "path";
4402
+ import { readFileSync as readFileSync17 } from "fs";
4403
+ import { join as join21 } from "path";
4244
4404
 
4245
4405
  // src/roles/typescript/typecheck-script.ts
4246
4406
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -4262,10 +4422,16 @@ function keepsForeignChecker(existing) {
4262
4422
 
4263
4423
  // src/roles/typescript/adapters/tsc/plan.ts
4264
4424
  var INSTALL_NOTE = "run `pnpm install` to fetch @hublo/sentinel (added to the module devDependencies) so `extends` and the typecheck script resolve";
4265
- var TYPECHECK_TARGET = { cache: true, inputs: ["default", "{projectRoot}/tsconfig.json"] };
4425
+ var TYPECHECK_TARGET = {
4426
+ cache: true,
4427
+ inputs: ["default", "^default", "{projectRoot}/tsconfig.json"]
4428
+ };
4266
4429
  function typecheckScripts(cwd) {
4267
4430
  return {
4268
- [TYPECHECK_SCRIPT_NAME]: composeTypecheckScript(moduleScripts(cwd)[TYPECHECK_SCRIPT_NAME])
4431
+ // Reads the nx target too, not only the npm script: five modules in this repo keep a
4432
+ // real prerequisite (the brand-token generator) in an `nx:run-commands` typecheck
4433
+ // target, and adoption used to delete it along with the target.
4434
+ [TYPECHECK_SCRIPT_NAME]: composeTypecheckScript(existingCommand(cwd, TYPECHECK_SCRIPT_NAME))
4269
4435
  };
4270
4436
  }
4271
4437
  function composeExtends(current, preset) {
@@ -4325,7 +4491,7 @@ function planAdoption(context) {
4325
4491
  };
4326
4492
  }
4327
4493
  const existing = parseJsonc(
4328
- readFileSync15(join19(context.cwd, target.path), "utf8"),
4494
+ readFileSync17(join21(context.cwd, target.path), "utf8"),
4329
4495
  target.path
4330
4496
  );
4331
4497
  const extendsChain = composeExtends(existing.extends, preset);
@@ -4463,7 +4629,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4463
4629
  let chain;
4464
4630
  try {
4465
4631
  const parsed = parseJsonc(
4466
- readFileSync16(join20(cwd, target.path), "utf8"),
4632
+ readFileSync18(join22(cwd, target.path), "utf8"),
4467
4633
  target.path
4468
4634
  );
4469
4635
  chain = parsed.extends;
@@ -4476,7 +4642,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4476
4642
  );
4477
4643
  if (preset === void 0) return void 0;
4478
4644
  try {
4479
- createRequire4(join20(cwd, "noop.js")).resolve(preset);
4645
+ createRequire4(join22(cwd, "noop.js")).resolve(preset);
4480
4646
  return void 0;
4481
4647
  } catch {
4482
4648
  return preset;
@@ -4572,7 +4738,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4572
4738
  referencedProjects(cwd, config) {
4573
4739
  try {
4574
4740
  const parsed = parseJsonc(
4575
- readFileSync16(join20(cwd, config), "utf8"),
4741
+ readFileSync18(join22(cwd, config), "utf8"),
4576
4742
  config
4577
4743
  );
4578
4744
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -4588,7 +4754,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4588
4754
  * check.
4589
4755
  */
4590
4756
  typecheckTarget(cwd) {
4591
- if (existsSync17(join20(cwd, "tsconfig.json"))) return "tsconfig.json";
4757
+ if (existsSync18(join22(cwd, "tsconfig.json"))) return "tsconfig.json";
4592
4758
  const target = resolveTsconfigTarget(cwd);
4593
4759
  return target.reason === "none" ? null : target.path;
4594
4760
  }
@@ -4791,8 +4957,8 @@ function replaceLines(current, replacements) {
4791
4957
  }
4792
4958
 
4793
4959
  // src/core/apply-plan.ts
4794
- import { existsSync as existsSync18, readFileSync as readFileSync17, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
4795
- import { resolve as resolve3, sep } from "path";
4960
+ import { existsSync as existsSync19, readFileSync as readFileSync19, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
4961
+ import { resolve as resolve4, sep } from "path";
4796
4962
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
4797
4963
 
4798
4964
  // src/shared/deep-merge.ts
@@ -4802,15 +4968,15 @@ function isPlainObject(value) {
4802
4968
 
4803
4969
  // src/core/apply-plan.ts
4804
4970
  function resolveWithinRoot(cwd, relativePath) {
4805
- const root = resolve3(cwd);
4806
- const absolutePath = resolve3(root, relativePath);
4971
+ const root = resolve4(cwd);
4972
+ const absolutePath = resolve4(root, relativePath);
4807
4973
  if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
4808
4974
  throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
4809
4975
  }
4810
4976
  return absolutePath;
4811
4977
  }
4812
4978
  function readIfExists(absolutePath) {
4813
- return existsSync18(absolutePath) ? readFileSync17(absolutePath, "utf8") : void 0;
4979
+ return existsSync19(absolutePath) ? readFileSync19(absolutePath, "utf8") : void 0;
4814
4980
  }
4815
4981
  function* leaves(value, prefix = []) {
4816
4982
  for (const [key, keyValue] of Object.entries(value)) {
@@ -4928,8 +5094,8 @@ function applyPlan(cwd, plan2) {
4928
5094
  }
4929
5095
 
4930
5096
  // src/core/config/preset-evidence.ts
4931
- import { existsSync as existsSync19, readdirSync as readdirSync3, readFileSync as readFileSync18 } from "fs";
4932
- import { join as join21 } from "path";
5097
+ import { existsSync as existsSync20, readdirSync as readdirSync3, readFileSync as readFileSync20 } from "fs";
5098
+ import { join as join23 } from "path";
4933
5099
  var PATH_SIGNALS = [
4934
5100
  {
4935
5101
  preset: "nest",
@@ -4943,11 +5109,11 @@ var DEPENDENCY_SIGNALS = [
4943
5109
  { preset: "nest", pattern: /^@nestjs\// }
4944
5110
  ];
4945
5111
  function dependencyNames(cwd) {
4946
- const path = join21(cwd, "package.json");
4947
- if (!existsSync19(path)) return [];
5112
+ const path = join23(cwd, "package.json");
5113
+ if (!existsSync20(path)) return [];
4948
5114
  try {
4949
5115
  const manifest = parseJsonc(
4950
- readFileSync18(path, "utf8"),
5116
+ readFileSync20(path, "utf8"),
4951
5117
  path
4952
5118
  );
4953
5119
  return [
@@ -4970,7 +5136,7 @@ function declaresJsx(cwd) {
4970
5136
  for (const name of entries) {
4971
5137
  try {
4972
5138
  const config = parseJsonc(
4973
- readFileSync18(join21(cwd, name), "utf8"),
5139
+ readFileSync20(join23(cwd, name), "utf8"),
4974
5140
  name
4975
5141
  );
4976
5142
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -5120,6 +5286,7 @@ export {
5120
5286
  WORKSPACE_ROOT_MARKER,
5121
5287
  findWorkspaceRoot,
5122
5288
  ensureWorkspacePrep,
5289
+ inspectWorkspacePrep,
5123
5290
  palette,
5124
5291
  resolveBin,
5125
5292
  VERBS,
@@ -5130,4 +5297,4 @@ export {
5130
5297
  detectFramework,
5131
5298
  dispatch
5132
5299
  };
5133
- //# sourceMappingURL=chunk-HNE774F7.js.map
5300
+ //# sourceMappingURL=chunk-RGIEWANO.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-HNE774F7.js";
10
+ } from "./chunk-RGIEWANO.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
package/oxlint/nest.json CHANGED
@@ -327,7 +327,7 @@
327
327
  ],
328
328
  "typescript/return-await": [
329
329
  "warn",
330
- "never"
330
+ "in-try-catch"
331
331
  ],
332
332
  "typescript/switch-exhaustiveness-check": [
333
333
  "error",
@@ -319,7 +319,7 @@
319
319
  "typescript/no-useless-constructor": "warn",
320
320
  "typescript/return-await": [
321
321
  "warn",
322
- "never"
322
+ "in-try-catch"
323
323
  ],
324
324
  "use-isnan": [
325
325
  "error",
package/oxlint/react.json CHANGED
@@ -438,7 +438,7 @@
438
438
  "typescript/restrict-template-expressions": "error",
439
439
  "typescript/return-await": [
440
440
  "warn",
441
- "never"
441
+ "in-try-catch"
442
442
  ],
443
443
  "typescript/switch-exhaustiveness-check": "warn",
444
444
  "typescript/unbound-method": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",