@hublo/sentinel 1.2.0-alpha.0 → 1.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  REACT_APP_DEFAULTS,
3
+ REQUIREMENTS,
3
4
  isPlainObject
4
- } from "./chunk-XSCDJZDY.js";
5
+ } from "./chunk-XVDOQ3G3.js";
5
6
 
6
7
  // src/core/registry.ts
7
8
  var adapters = [];
@@ -133,20 +134,6 @@ function readNxProjectName(dir) {
133
134
  }
134
135
  }
135
136
 
136
- // src/shared/color.ts
137
- import { styleText } from "util";
138
- function palette(stream) {
139
- const paint = (format, text) => styleText(format, text, { stream });
140
- return {
141
- ok: (text) => paint(["green", "bold"], text),
142
- fail: (text) => paint(["red", "bold"], text),
143
- warn: (text) => paint("yellow", text),
144
- strong: (text) => paint("bold", text),
145
- dim: (text) => paint("dim", text),
146
- accent: (text) => paint("cyan", text)
147
- };
148
- }
149
-
150
137
  // src/core/domain.ts
151
138
  var VERBS = ["run", "inspect", "init", "migrate", "report", "status"];
152
139
  var TARGETS = [
@@ -166,7 +153,21 @@ var SWEEPABLE_TARGETS = TARGETS.filter(
166
153
  );
167
154
  var PRESET_NAMES = ["react", "nest", "svelte", "node", "tools"];
168
155
 
169
- // src/roles/build/adapters/vite/vite-dev.adapter.ts
156
+ // src/shared/color.ts
157
+ import { styleText } from "util";
158
+ function palette(stream) {
159
+ const paint = (format, text) => styleText(format, text, { stream });
160
+ return {
161
+ ok: (text) => paint(["green", "bold"], text),
162
+ fail: (text) => paint(["red", "bold"], text),
163
+ warn: (text) => paint("yellow", text),
164
+ strong: (text) => paint("bold", text),
165
+ dim: (text) => paint("dim", text),
166
+ accent: (text) => paint("cyan", text)
167
+ };
168
+ }
169
+
170
+ // src/roles/build/adapters/vite/vite-role.adapter.ts
170
171
  import { spawnSync } from "child_process";
171
172
 
172
173
  // src/core/config/tool-args.ts
@@ -206,9 +207,9 @@ function binFromOwnInstall(packageName, binName) {
206
207
  try {
207
208
  const manifest = require2.resolve(`${packageName}/package.json`);
208
209
  const bin = require2(manifest).bin;
209
- const relative3 = typeof bin === "string" ? bin : bin?.[binName];
210
- if (!relative3) return void 0;
211
- const executable = join2(dirname2(manifest), relative3);
210
+ const relative4 = typeof bin === "string" ? bin : bin?.[binName];
211
+ if (!relative4) return void 0;
212
+ const executable = join2(dirname2(manifest), relative4);
212
213
  return existsSync3(executable) ? executable : void 0;
213
214
  } catch {
214
215
  return void 0;
@@ -399,7 +400,7 @@ function buildTarget() {
399
400
  return {
400
401
  [BUILD_SCRIPT_NAME]: {
401
402
  cache: true,
402
- inputs: ["default", "^default", `{projectRoot}/${BUILD_CONFIG_FILES[0]}`]
403
+ inputs: ["default", "^default", ...BUILD_CONFIG_FILES.map((name) => `{projectRoot}/${name}`)]
403
404
  }
404
405
  };
405
406
  }
@@ -417,17 +418,17 @@ var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
417
418
  var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
418
419
  function appOptions(rest) {
419
420
  const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
420
- const kept = tokens.filter((token, at) => !(at === 0 && DEV_SUBCOMMANDS.has(token)));
421
+ const kept = tokens.filter((token, at2) => !(at2 === 0 && DEV_SUBCOMMANDS.has(token)));
421
422
  return kept.join(" ");
422
423
  }
423
424
  function composeDevScript(existing, command) {
424
425
  if (existing === void 0 || existing.trim() === "") return command;
425
426
  const chunks = existing.trim().split(CHAIN);
426
- const at = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
427
- if (at === -1) return existing;
428
- const rest = (chunks[at] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
427
+ const at2 = chunks.map((chunk) => VITE_TOKEN.test(chunk)).lastIndexOf(true);
428
+ if (at2 === -1) return existing;
429
+ const rest = (chunks[at2] ?? "").replace(/^.*?(^|\s|\/)vite(\s|$)/, "");
429
430
  const options = appOptions(rest);
430
- chunks[at] = options === "" ? command : `${command} -- ${options}`;
431
+ chunks[at2] = options === "" ? command : `${command} -- ${options}`;
431
432
  return chunks.join(CHAIN);
432
433
  }
433
434
 
@@ -446,17 +447,24 @@ var NOT_ADOPTED = (configFile, unreadable = null) => ({
446
447
  function buildConfigFile(cwd) {
447
448
  return BUILD_CONFIG_FILES.find((name) => existsSync6(join6(cwd, name)));
448
449
  }
450
+ function importsSpecifier(source, specifier) {
451
+ const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
452
+ return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
453
+ }
449
454
  function importsPreset(source) {
450
- const specifier = BUILD_PRESET_SPECIFIER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
451
- return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${specifier}['"\`]`).test(source);
455
+ return importsSpecifier(source, BUILD_PRESET_SPECIFIER);
452
456
  }
453
457
  var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
454
458
  function declaredBuildPreset(cwd) {
459
+ const source = readBuildConfigSource(cwd);
460
+ if (source === void 0) return void 0;
461
+ return REACT_PLUGIN_SPECIFIERS.some((specifier) => importsSpecifier(source, specifier)) ? "react" : void 0;
462
+ }
463
+ function readBuildConfigSource(cwd) {
455
464
  const configFile = buildConfigFile(cwd);
456
465
  if (!configFile) return void 0;
457
466
  try {
458
- const source = readFileSync4(join6(cwd, configFile), "utf8");
459
- return REACT_PLUGIN_SPECIFIERS.some((specifier) => source.includes(specifier)) ? "react" : void 0;
467
+ return readFileSync4(join6(cwd, configFile), "utf8");
460
468
  } catch {
461
469
  return void 0;
462
470
  }
@@ -489,6 +497,34 @@ function readBuildAdoption(cwd) {
489
497
  };
490
498
  }
491
499
 
500
+ // src/roles/build/requirements.ts
501
+ function atLeast(declared, needed) {
502
+ const parse2 = (value) => value.replace(/^[\^~>=<\s]+/, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
503
+ const left = parse2(declared);
504
+ const right = parse2(needed);
505
+ for (let at2 = 0; at2 < Math.max(left.length, right.length); at2++) {
506
+ const a = left[at2] ?? 0;
507
+ const b = right[at2] ?? 0;
508
+ if (a !== b) return a > b;
509
+ }
510
+ return true;
511
+ }
512
+ function unmetRequirements(preset, declared) {
513
+ const entry = REQUIREMENTS[preset];
514
+ if (!entry) return [];
515
+ return Object.entries(entry.requires).filter(([name, needed]) => {
516
+ const have = declared[name];
517
+ return have === void 0 || !atLeast(have, needed);
518
+ }).map(([name, needed]) => ({ name, declared: declared[name], needed }));
519
+ }
520
+ function describeUnmet(preset, unmet) {
521
+ const entry = REQUIREMENTS[preset];
522
+ const list = unmet.map(
523
+ ({ name, declared, needed }) => `${name} ${declared === void 0 ? "is not declared here" : `is at ${declared}`}, needs >= ${needed}`
524
+ ).join("; ");
525
+ return `the build role does not apply to this module yet: ${list}. ${entry?.why ?? ""}. Nothing was written. Raise those versions, check the module still builds and its tests still pass, then re-run \`sentinel --init --build\`.`;
526
+ }
527
+
492
528
  // src/roles/build/plan.ts
493
529
  function scaffold() {
494
530
  return `import { defineConfig, reactApp } from '${BUILD_PRESET_SPECIFIER}'
@@ -497,8 +533,8 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite'
497
533
  import path from 'node:path'
498
534
 
499
535
  /*
500
- * Composition comes from sentinel: how the env is loaded, which plugins run under test and
501
- * which under a real build, the order they run in. The values below are this app's own.
536
+ * Composition comes from sentinel: which plugins run under test and which under a real build,
537
+ * the order they run in, and how the env is read. The values below are this app's own.
502
538
  *
503
539
  * TanStack is passed IN rather than imported by sentinel, because the app codes against it
504
540
  * directly. \`alias\` is passed through verbatim and never read, so nothing in it can be lost.
@@ -520,6 +556,10 @@ export default defineConfig(({ mode }) =>
520
556
  alias: [],
521
557
  }),
522
558
  )
559
+
560
+ // If this app reads environment variables, use \`appEnv(__dirname, mode)\` rather than Vite's
561
+ // \`loadEnv\`: it applies the production -> prd normalisation this repo deploys under, which
562
+ // every app was otherwise retyping.
523
563
  `;
524
564
  }
525
565
  function ownedBuildDependencies(cwd) {
@@ -558,10 +598,18 @@ function buildScripts(cwd) {
558
598
  return scripts;
559
599
  }
560
600
  function plan(context) {
601
+ const manifest = readProjectPackageJson(context.cwd);
602
+ const unmet = unmetRequirements(context.preset, {
603
+ ...manifest.dependencies,
604
+ ...manifest.devDependencies
605
+ });
606
+ if (unmet.length > 0) {
607
+ return { operations: [], skipped: describeUnmet(context.preset, unmet) };
608
+ }
561
609
  if (declaredBuildPreset(context.cwd) === void 0 && buildConfigFile(context.cwd) !== void 0) {
562
610
  return {
563
611
  operations: [],
564
- skipped: `this module has a Vite config, but it does not build a React app (no @vitejs/plugin-react or @tanstack/react-start in it). The build role ships a React preset only; the two SvelteKit configs in this repo share nothing with it.`
612
+ skipped: `this module has a Vite config, but it does not build a React app (no @vitejs/plugin-react or @tanstack/react-start in it). The build role ships a React preset only.`
565
613
  };
566
614
  }
567
615
  const notes = [];
@@ -598,83 +646,150 @@ function readsPreset(cwd, configFile) {
598
646
  }
599
647
 
600
648
  // src/roles/build/resolve-vite.ts
601
- function resolveVite(cwd) {
602
- return binFromOwnInstall("vite", "vite") ?? resolveBin(cwd, "vite");
649
+ function resolveViteWith(cwd) {
650
+ const own = binFromOwnInstall("vite", "vite");
651
+ if (own) return { bin: own, origin: "sentinel" };
652
+ const module_ = resolveBin(cwd, "vite");
653
+ return module_ ? { bin: module_, origin: "module" } : { bin: void 0, origin: "none" };
603
654
  }
604
655
  function viteOrigin(cwd) {
605
- if (binFromOwnInstall("vite", "vite")) return "sentinel";
606
- return resolveBin(cwd, "vite") ? "module" : "none";
656
+ return resolveViteWith(cwd).origin;
607
657
  }
608
658
 
609
- // src/roles/build/adapters/vite/vite-dev.adapter.ts
610
- var ViteDevAdapter = class extends BaseAdapter {
611
- target = "dev";
659
+ // src/roles/build/adapters/vite/vite-role.adapter.ts
660
+ var VITE_VALUE_FLAGS = [
661
+ "--config",
662
+ "-c",
663
+ "--mode",
664
+ "-m",
665
+ "--outDir",
666
+ "--logLevel",
667
+ "--base",
668
+ "--host",
669
+ "--port"
670
+ ];
671
+ var ViteRoleAdapter = class extends BaseAdapter {
612
672
  runner = "vite";
673
+ /**
674
+ * The presets this role has something to SAY about, which is wider than the ones it serves.
675
+ *
676
+ * React it serves. Svelte it does not, yet, and the difference matters to a developer: the
677
+ * plugin caps at Vite 6 until its major 7, which needs `svelte ^5.46.4`, so the role applies
678
+ * once the module moves. `plan` reports that chain and SKIPS, which it can only do if
679
+ * resolution let it run at all — filtering here would give "no adapter for preset svelte",
680
+ * which tells nobody what to do about it.
681
+ *
682
+ * Nest, node and tools are genuinely declined: there is nothing to bundle, and no version
683
+ * would change that.
684
+ */
613
685
  appliesTo(preset) {
614
- return preset === "react";
686
+ return preset === "react" || preset === "svelte";
615
687
  }
688
+ /**
689
+ * `react`, read from the module's Vite config rather than from its dependencies.
690
+ *
691
+ * Without this the role is unreachable in practice: the toolchain is declared at the ROOT,
692
+ * so every front app detects as `node` and `appliesTo` filters the adapter out — including
693
+ * during `sentinel --inspect` with no target, which never passes `--preset`.
694
+ */
616
695
  declaredPreset(cwd) {
617
696
  return declaredBuildPreset(cwd);
618
697
  }
619
698
  /**
620
- * The same plan as `--build`, deliberately.
699
+ * The same plan for both targets, deliberately.
621
700
  *
622
- * `build` and `serve` are one app's two calls into one toolchain, so adopting one without
623
- * the other leaves the module half-migrated in a way nothing reports. The operations are
624
- * idempotent, so `--init --build` and `--init --dev` are interchangeable rather than
625
- * additive.
701
+ * `build` and `serve` are one app's two calls into one toolchain, so adopting one without the
702
+ * other leaves the module half-migrated in a way nothing reports. The operations are
703
+ * idempotent, so `--init --build` and `--init --dev` are interchangeable rather than additive.
626
704
  */
627
705
  plan(context) {
628
706
  return plan(context);
629
707
  }
708
+ async status(ctx) {
709
+ const { adopted, preset, conformant, drift, unreadable } = readBuildAdoption(ctx.cwd);
710
+ return { adopted, preset, conformant, drift, unreadable };
711
+ }
630
712
  /**
631
- * Start the dev server. It does not return until stopped, so there is no verdict to report
632
- * beyond the exit code the developer's own Ctrl-C produces.
713
+ * Adoption, the binary, and the warning about which copy of Vite is running.
633
714
  *
634
- * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
635
- * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
636
- * running it again would race the watcher that is about to own the file.
715
+ * A module that has not adopted is reported and PASSES, the same stance every other role
716
+ * takes: `--run` with no named target sweeps every wired target across the workspace, so
717
+ * failing here would exit non-zero on every module that has not migrated, which is most of
718
+ * them.
637
719
  */
638
- async run(ctx) {
720
+ prepare(ctx) {
639
721
  if (!readBuildAdoption(ctx.cwd).adopted) {
640
- process.stderr.write(
641
- `sentinel dev(vite): no sentinel preset in this module's Vite config; run \`sentinel --init --build\` to adopt.
642
- `
722
+ this.say(
723
+ `no sentinel preset in this module's Vite config; run \`sentinel --init --build\` to adopt.`
643
724
  );
644
- return { ok: true, code: 0 };
725
+ return { kind: "stop", result: { ok: true, code: 0 } };
645
726
  }
646
- const vite = resolveVite(ctx.cwd);
647
- if (!vite) {
648
- process.stderr.write(
649
- `sentinel dev(vite): could not find the vite binary (looked in ${binSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.
650
- `
727
+ const { bin, origin } = resolveViteWith(ctx.cwd);
728
+ if (!bin) {
729
+ this.say(
730
+ `could not find the vite binary (looked in ${binSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.`
651
731
  );
652
- return { ok: false, code: 1 };
732
+ return { kind: "stop", result: { ok: false, code: 1 } };
653
733
  }
654
- if (viteOrigin(ctx.cwd) === "module") {
655
- process.stderr.write(
656
- `sentinel dev(vite): serving with the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite in one process fail in ways that never mention a version. Remove vite from this module's package.json.
657
- `
734
+ if (origin === "module") {
735
+ this.say(
736
+ `using the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite fail in ways that never mention a version. Remove vite from this module's package.json.`
658
737
  );
659
738
  }
660
- const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, { valueFlags: [] });
661
- const result = spawnSync(vite, [...options, ...paths], { cwd: ctx.cwd, stdio: "inherit" });
739
+ return { kind: "ready", vite: bin };
740
+ }
741
+ /**
742
+ * Run Vite with the caller's own arguments after `--`, and report what happened.
743
+ *
744
+ * `whenSignalled` is the only difference between the two targets here, and it is a real one:
745
+ * a build killed by a signal did not produce a bundle, while a dev server killed by Ctrl-C
746
+ * did exactly what the developer asked.
747
+ */
748
+ spawnVite(ctx, vite, argv, whenSignalled) {
749
+ const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
750
+ valueFlags: VITE_VALUE_FLAGS
751
+ });
752
+ const result = spawnSync(vite, [...argv, ...options, ...paths], {
753
+ cwd: ctx.cwd,
754
+ stdio: "inherit"
755
+ });
662
756
  if (result.error) {
663
- process.stderr.write(`sentinel dev(vite): could not run vite (${result.error.message})
664
- `);
757
+ this.say(`could not run vite (${result.error.message})`);
665
758
  return { ok: false, code: 1 };
666
759
  }
667
- const code = result.status ?? 0;
760
+ const code = result.status ?? whenSignalled;
668
761
  return { ok: code === 0, code };
669
762
  }
670
- async status(ctx) {
671
- const { adopted, preset, conformant, drift, unreadable } = readBuildAdoption(ctx.cwd);
672
- return { adopted, preset, conformant, drift, unreadable };
763
+ /** One prefix for every message this role emits, so they cannot drift apart. */
764
+ say(message) {
765
+ process.stderr.write(`sentinel ${this.label}(vite): ${message}
766
+ `);
673
767
  }
674
768
  };
675
769
 
676
- // src/roles/build/adapters/vite/vite.adapter.ts
677
- import { spawnSync as spawnSync3 } from "child_process";
770
+ // src/roles/build/adapters/vite/vite-dev.adapter.ts
771
+ var ViteDevAdapter = class extends ViteRoleAdapter {
772
+ target = "dev";
773
+ get label() {
774
+ return "dev";
775
+ }
776
+ /**
777
+ * Start the dev server. It does not return until stopped, so there is no verdict to report
778
+ * beyond the exit code the developer's own Ctrl-C produces.
779
+ *
780
+ * No `prebuild` here, unlike `--run --build`: the artefact this repo generates before a build
781
+ * is produced by the watcher that WRAPS this command (`run-with-runtime-artifact-watch`), and
782
+ * running it again would race the watcher that is about to own the file.
783
+ *
784
+ * No subcommand either. `vite`, `vite dev` and `vite serve` all start the server, and the
785
+ * bare form is the one every version accepts.
786
+ */
787
+ async run(ctx) {
788
+ const ready = this.prepare(ctx);
789
+ if (ready.kind === "stop") return ready.result;
790
+ return this.spawnVite(ctx, ready.vite, [], 0);
791
+ }
792
+ };
678
793
 
679
794
  // src/roles/build/owned-paths.ts
680
795
  var PRESET_OWNED_KEYS = [
@@ -683,6 +798,7 @@ var PRESET_OWNED_KEYS = [
683
798
  "define",
684
799
  "server",
685
800
  "build",
801
+ "nitro",
686
802
  "resolve",
687
803
  "plugins"
688
804
  ];
@@ -714,7 +830,7 @@ function asRecord(value) {
714
830
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
715
831
  }
716
832
  function sameStrings(a, b) {
717
- return a.length === b.length && a.every((value, at) => value === b[at]);
833
+ return a.length === b.length && a.every((value, at2) => value === b[at2]);
718
834
  }
719
835
 
720
836
  // src/roles/build/prerequisite.ts
@@ -761,83 +877,28 @@ async function resolveBuildConfig(cwd) {
761
877
  }
762
878
 
763
879
  // src/roles/build/adapters/vite/vite.adapter.ts
764
- var VITE_VALUE_FLAGS = ["--config", "-c", "--mode", "-m", "--outDir", "--logLevel", "--base"];
765
- var ViteAdapter = class extends BaseAdapter {
880
+ var ViteAdapter = class extends ViteRoleAdapter {
766
881
  target = "build";
767
- runner = "vite";
768
- /**
769
- * React only. The two SvelteKit configs in this repo are 34 and 21 lines and share nothing
770
- * with the React three; claiming them would mean a preset that fits neither.
771
- */
772
- appliesTo(preset) {
773
- return preset === "react";
774
- }
775
- /**
776
- * `react`, read from the module's Vite config rather than from its dependencies.
777
- *
778
- * Without this the role is unreachable in practice: the toolchain is declared at the ROOT,
779
- * so every front app detects as `node` and `appliesTo` filters the adapter out — including
780
- * during `sentinel --inspect` with no target, which never passes `--preset`.
781
- */
782
- declaredPreset(cwd) {
783
- return declaredBuildPreset(cwd);
784
- }
785
- plan(context) {
786
- return plan(context);
882
+ get label() {
883
+ return "build";
787
884
  }
788
885
  /**
789
886
  * Build the module.
790
887
  *
791
- * A module that has not adopted is reported and passes, the same stance every other role
792
- * takes: `--run` with no named target sweeps every wired target across the workspace, so
793
- * failing here would exit non-zero on every module that has not migrated, which is most of
794
- * them.
888
+ * Adoption, the binary and the two-copies warning are `prepare`'s, shared with `--dev`. What
889
+ * is this target's own is the `prebuild` step and the `build` subcommand.
795
890
  */
796
891
  async run(ctx) {
797
- const adoption = readBuildAdoption(ctx.cwd);
798
- if (!adoption.adopted) {
799
- process.stderr.write(
800
- `sentinel build(vite): no sentinel preset in this module's Vite config; run \`sentinel --init --build\` to adopt.
801
- `
802
- );
803
- return { ok: true, code: 0 };
804
- }
892
+ const ready = this.prepare(ctx);
893
+ if (ready.kind === "stop") return ready.result;
805
894
  const prerequisite = runPrerequisite(ctx.cwd);
806
895
  if (prerequisite.kind === "failed") {
807
- process.stderr.write(
808
- `sentinel build(vite): the ${PREBUILD_SCRIPT_NAME} step failed, so the build was not started: ${prerequisite.reason}
809
- `
896
+ this.say(
897
+ `the ${PREBUILD_SCRIPT_NAME} step failed, so the build was not started: ${prerequisite.reason}`
810
898
  );
811
899
  return { ok: false, code: prerequisite.code };
812
900
  }
813
- const vite = resolveVite(ctx.cwd);
814
- if (!vite) {
815
- process.stderr.write(
816
- `sentinel build(vite): could not find the vite binary (looked in ${binSearchPath(ctx.cwd)}). Run \`pnpm install\` in the module.
817
- `
818
- );
819
- return { ok: false, code: 1 };
820
- }
821
- if (viteOrigin(ctx.cwd) === "module") {
822
- process.stderr.write(
823
- `sentinel build(vite): building with the MODULE's vite, not sentinel's. An adopted config gets its plugins from sentinel, and two copies of Vite in one build fail in ways that never mention a version. Remove vite from this module's package.json.
824
- `
825
- );
826
- }
827
- const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
828
- valueFlags: VITE_VALUE_FLAGS
829
- });
830
- const result = spawnSync3(vite, ["build", ...options, ...paths], {
831
- cwd: ctx.cwd,
832
- stdio: "inherit"
833
- });
834
- if (result.error) {
835
- process.stderr.write(`sentinel build(vite): could not run vite (${result.error.message})
836
- `);
837
- return { ok: false, code: 1 };
838
- }
839
- const code = result.status ?? 1;
840
- return { ok: code === 0, code };
901
+ return this.spawnVite(ctx, ready.vite, ["build"], 1);
841
902
  }
842
903
  /**
843
904
  * What this module builds with, without building it.
@@ -883,17 +944,19 @@ var ViteAdapter = class extends BaseAdapter {
883
944
  departures: describeDepartures(config, REACT_APP_DEFAULTS),
884
945
  additions: appAdditions(config),
885
946
  resolved: {
886
- base: config.base ?? null,
887
- target: config.build?.target ?? null,
888
- sourcemap: config.build?.sourcemap ?? null,
889
- port: config.server?.port ?? null,
947
+ base: at(config, "base") ?? null,
948
+ target: at(config, "build", "target") ?? null,
949
+ sourcemap: at(config, "build", "sourcemap") ?? null,
950
+ port: at(config, "server", "port") ?? null,
890
951
  // The one number nothing else reports and that no formula predicts: career goes DOWN
891
952
  // to 9998 where the others go up, which is why it is data rather than `port + 1`.
892
- hmrPort: config.server?.hmr?.port ?? null,
953
+ hmrPort: at(config, "server", "hmr", "port") ?? null,
893
954
  // Counted, not listed. It is 56-67% of every config today, and printing 296 entries
894
955
  // would bury everything above it. The count is what tells you whether it moved.
895
- aliases: Array.isArray(config.resolve?.alias) ? config.resolve.alias.length : 0,
896
- plugins: Array.isArray(config.plugins) ? config.plugins.flat(9).length : 0
956
+ aliases: countOf(at(config, "resolve", "alias")),
957
+ // Flattened without a depth limit: Vite lets a plugin be an arbitrarily nested array,
958
+ // and a magic number here would silently undercount the day someone nests one deeper.
959
+ plugins: countOf(at(config, "plugins"))
897
960
  }
898
961
  };
899
962
  }
@@ -920,11 +983,18 @@ var ViteAdapter = class extends BaseAdapter {
920
983
  }
921
984
  };
922
985
  }
923
- async status(ctx) {
924
- const { adopted, preset, conformant, drift, unreadable } = readBuildAdoption(ctx.cwd);
925
- return { adopted, preset, conformant, drift, unreadable };
926
- }
927
986
  };
987
+ function at(config, ...path) {
988
+ let value = config;
989
+ for (const key of path) {
990
+ if (typeof value !== "object" || value === null) return void 0;
991
+ value = value[key];
992
+ }
993
+ return value;
994
+ }
995
+ function countOf(value) {
996
+ return Array.isArray(value) ? value.flat(Number.POSITIVE_INFINITY).length : 0;
997
+ }
928
998
 
929
999
  // src/roles/build/register.ts
930
1000
  function registerBuild() {
@@ -935,13 +1005,13 @@ function registerBuild() {
935
1005
  }
936
1006
 
937
1007
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
938
- import { spawnSync as spawnSync4 } from "child_process";
1008
+ import { spawnSync as spawnSync3 } from "child_process";
939
1009
  import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
940
1010
  import { join as join14 } from "path";
941
1011
 
942
1012
  // src/core/config/has-source.ts
943
1013
  import { readdirSync } from "fs";
944
- import { extname, join as join8 } from "path";
1014
+ import { extname, join as join8, relative } from "path";
945
1015
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
946
1016
  "node_modules",
947
1017
  "dist",
@@ -1000,6 +1070,35 @@ function hasSourceFiles(cwd, extensions) {
1000
1070
  }
1001
1071
  return false;
1002
1072
  }
1073
+ function listSourceFiles(cwd, extensions) {
1074
+ const wanted = new Set(extensions);
1075
+ const queue = [cwd];
1076
+ const found = [];
1077
+ while (queue.length > 0) {
1078
+ const dir = queue.pop();
1079
+ let entries;
1080
+ try {
1081
+ entries = readdirSync(dir, { withFileTypes: true });
1082
+ } catch {
1083
+ continue;
1084
+ }
1085
+ for (const entry of entries) {
1086
+ const full = join8(dir, entry.name);
1087
+ if (entry.isDirectory()) {
1088
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(full);
1089
+ continue;
1090
+ }
1091
+ if (wanted.has(extname(entry.name))) found.push(relative(cwd, full));
1092
+ }
1093
+ }
1094
+ return found;
1095
+ }
1096
+
1097
+ // src/core/named-scope.ts
1098
+ var TOLERATE_EMPTY_FLAG = "--no-error-on-unmatched-pattern";
1099
+ function scopeFlags(paths) {
1100
+ return paths.length > 0 ? [TOLERATE_EMPTY_FLAG] : [];
1101
+ }
1003
1102
 
1004
1103
  // src/core/settings.ts
1005
1104
  var WORKSPACE_ROOT_MARKER = "nx.json";
@@ -1007,7 +1106,7 @@ var DEFAULT_MAX_DIAGNOSTICS = 100;
1007
1106
 
1008
1107
  // src/core/workspace-prep.ts
1009
1108
  import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
1010
- import { dirname as dirname3, join as join9, relative } from "path";
1109
+ import { dirname as dirname3, join as join9, relative as relative2 } from "path";
1011
1110
  var OVERRIDE_KEY = "i18next>typescript";
1012
1111
  var NATIVE_TS_ALIAS = "@typescript/native";
1013
1112
  var WORKSPACE_YAML = "pnpm-workspace.yaml";
@@ -1080,7 +1179,7 @@ var ROOT_PRETTIER_CONFIGS = [
1080
1179
  "prettier.config.mjs"
1081
1180
  ];
1082
1181
  function ensureFormatterExclusion(root, moduleDir, dryRun) {
1083
- const rel = relative(root, moduleDir).replaceAll("\\", "/");
1182
+ const rel = relative2(root, moduleDir).replaceAll("\\", "/");
1084
1183
  if (rel === "" || rel.startsWith("..")) return void 0;
1085
1184
  const ignorePath = join9(root, PRETTIER_IGNORE);
1086
1185
  const hasPrettier = existsSync8(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync8(join9(root, name)));
@@ -1527,6 +1626,22 @@ var OxfmtAdapter = class extends BaseAdapter {
1527
1626
  appliesTo(_preset) {
1528
1627
  return true;
1529
1628
  }
1629
+ /**
1630
+ * The preset this module COMMITTED to, read from the `$sentinel` block it writes.
1631
+ *
1632
+ * Every other role implements this and format did not, so it was the only one answering from
1633
+ * dependency detection alone. On an adopted Nest module whose own `.oxfmtrc.json` records
1634
+ * `"preset": "nest"`, `--inspect` reported `flavour=node` — the module knew, and the role did
1635
+ * not pass it on. Same command, four rows, two different answers about what the module is.
1636
+ *
1637
+ * It reads the RECORD rather than re-deriving: the block is what `--init` wrote, so it is the
1638
+ * module's declaration, and a re-derivation from hoisted dependencies is exactly the wrong
1639
+ * answer this exists to avoid.
1640
+ */
1641
+ declaredPreset(cwd) {
1642
+ const declared = readFormatAdoption(cwd).preset;
1643
+ return declared !== null && PRESET_NAMES.includes(declared) ? declared : void 0;
1644
+ }
1530
1645
  plan(context) {
1531
1646
  if (!hasSourceFiles(context.cwd, FORMATTABLE_EXTENSIONS)) {
1532
1647
  return {
@@ -1628,7 +1743,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1628
1743
  async afterInit(ctx) {
1629
1744
  const oxfmt = resolveOxfmt(ctx.cwd);
1630
1745
  if (!oxfmt) return;
1631
- const pass = spawnSync4(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1746
+ const pass = spawnSync3(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1632
1747
  process.stderr.write(` ${palette(process.stderr).dim(FORMATTER_DIFFERENCES)}
1633
1748
  `);
1634
1749
  if (pass.status === 0) return;
@@ -1691,10 +1806,14 @@ var OxfmtAdapter = class extends BaseAdapter {
1691
1806
  const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
1692
1807
  valueFlags: OXFMT_VALUE_FLAGS
1693
1808
  });
1694
- const result = spawnSync4(oxfmt, [mode, ...options, ...paths.length > 0 ? paths : ["."]], {
1695
- cwd: ctx.cwd,
1696
- stdio: "inherit"
1697
- });
1809
+ const result = spawnSync3(
1810
+ oxfmt,
1811
+ [mode, ...scopeFlags(paths), ...options, ...paths.length > 0 ? paths : ["."]],
1812
+ {
1813
+ cwd: ctx.cwd,
1814
+ stdio: "inherit"
1815
+ }
1816
+ );
1698
1817
  if (result.error) {
1699
1818
  process.stderr.write(
1700
1819
  `sentinel format(oxfmt): could not run oxfmt (${result.error.message})
@@ -1769,7 +1888,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1769
1888
  );
1770
1889
  return { ok: false, code: 1, metrics: { ...base, unformatted: null } };
1771
1890
  }
1772
- const result = spawnSync4(oxfmt, ["--list-different", "."], { cwd: ctx.cwd, encoding: "utf8" });
1891
+ const result = spawnSync3(oxfmt, ["--list-different", "."], { cwd: ctx.cwd, encoding: "utf8" });
1773
1892
  if (result.error) {
1774
1893
  process.stderr.write(
1775
1894
  `sentinel format(oxfmt): could not run oxfmt (${result.error.message})
@@ -1886,15 +2005,122 @@ function registerFormat() {
1886
2005
  }
1887
2006
 
1888
2007
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1889
- import { spawnSync as spawnSync5 } from "child_process";
1890
- import { existsSync as existsSync19, readFileSync as readFileSync18, rmSync, writeFileSync as writeFileSync2 } from "fs";
1891
- import { join as join22 } from "path";
2008
+ import { spawnSync as spawnSync4 } from "child_process";
2009
+ import { existsSync as existsSync20, readFileSync as readFileSync19, rmSync, writeFileSync as writeFileSync2 } from "fs";
2010
+ import { join as join24 } from "path";
1892
2011
 
1893
2012
  // src/core/config/deferred-rules.ts
1894
2013
  function deferredRuleNames(rules) {
1895
2014
  return rules.map((entry) => entry.rule);
1896
2015
  }
1897
2016
 
2017
+ // src/core/fix-report.ts
2018
+ import { statSync } from "fs";
2019
+ import { join as join15 } from "path";
2020
+ var CONFIG_REFUSED = /failed to parse .*config|invalid config file/i;
2021
+ function readFailure(output) {
2022
+ const lines = output.split("\n").map((line2) => line2.trim());
2023
+ if (!lines.some((line2) => CONFIG_REFUSED.test(line2))) return void 0;
2024
+ const detail = lines.find((line2) => /invalid config file/i.test(line2));
2025
+ const line = (detail ?? lines.find((entry) => entry !== "") ?? "").replace(/^x\s*/, "");
2026
+ return line.split(/:\s*Failed to parse config/i)[0] ?? line;
2027
+ }
2028
+ function fingerprint(path) {
2029
+ try {
2030
+ const stat = statSync(path);
2031
+ return `${stat.size}:${stat.mtimeMs}`;
2032
+ } catch {
2033
+ return void 0;
2034
+ }
2035
+ }
2036
+ function snapshotFiles(cwd, extensions) {
2037
+ const snapshot = /* @__PURE__ */ new Map();
2038
+ for (const file of listSourceFiles(cwd, extensions)) {
2039
+ const mark = fingerprint(join15(cwd, file));
2040
+ if (mark !== void 0) snapshot.set(file, mark);
2041
+ }
2042
+ return snapshot;
2043
+ }
2044
+ function changedSince(cwd, before, extensions) {
2045
+ const changed = [];
2046
+ for (const file of listSourceFiles(cwd, extensions)) {
2047
+ const now = fingerprint(join15(cwd, file));
2048
+ if (now === void 0) continue;
2049
+ if (before.get(file) !== now) changed.push(file);
2050
+ }
2051
+ return changed.sort();
2052
+ }
2053
+ var NAMED_IN_REPORT = 5;
2054
+ function describeFixOutcome(outcome, toolLabel) {
2055
+ if (outcome.failure !== void 0) {
2056
+ return `the ${toolLabel} autofix could not run, so nothing was fixed and this module has not been checked: ${outcome.failure}`;
2057
+ }
2058
+ if (outcome.changed.length === 0) return `the ${toolLabel} autofix changed nothing`;
2059
+ const named = outcome.changed.slice(0, NAMED_IN_REPORT).join(", ");
2060
+ const rest = outcome.changed.length - NAMED_IN_REPORT;
2061
+ const listed = rest > 0 ? `${named} and ${rest} more` : named;
2062
+ return `the ${toolLabel} autofix rewrote ${outcome.changed.length} file${outcome.changed.length === 1 ? "" : "s"}: ${listed}. Review the diff before committing: rules this module used to have switched off are enforced from now on.`;
2063
+ }
2064
+
2065
+ // src/roles/lint/module-baseline.ts
2066
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
2067
+ import { join as join16 } from "path";
2068
+ var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2069
+ var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2070
+ var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
2071
+ function holdsFrom(errorCounts) {
2072
+ return [...errorCounts].map(([rule, count]) => ({ rule, count })).sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule));
2073
+ }
2074
+ function renderBaseline(holds, version) {
2075
+ const entries = holds.map(({ rule }) => ` ${JSON.stringify(rule)}: "warn"`).join(",\n");
2076
+ return [
2077
+ "{",
2078
+ ` // Written by @hublo/sentinel@${version}. Do not edit by hand: \`sentinel --init --lint\``,
2079
+ " // regenerates it from a fresh measurement, and a rule you have since fixed drops out",
2080
+ " // and returns to `error` on its own.",
2081
+ " //",
2082
+ " // These rules are held at `warn` because this module ALREADY violated them when it",
2083
+ " // adopted. They still run and still report, so the build cannot fail on them.",
2084
+ " //",
2085
+ " // The hold is per RULE, not per occurrence: oxlint cannot freeze a specific list of",
2086
+ " // violations, so a NEW violation of one of these rules is covered too, and will only",
2087
+ " // warn. That is the cost of adopting without a red build, and the reason to clear this",
2088
+ " // file rather than live with it.",
2089
+ " //",
2090
+ " // `sentinel --inspect --lint` lists what is held; `sentinel --run --lint --json` counts",
2091
+ " // what is left, per rule. Fix them and re-run `--init --lint` to get the severity back.",
2092
+ ' "rules": {',
2093
+ entries,
2094
+ " }",
2095
+ "}",
2096
+ ""
2097
+ ].join("\n");
2098
+ }
2099
+ function extendsWithBaseline(current, hasHolds) {
2100
+ const withoutBaseline = current.filter((entry) => entry !== LINT_BASELINE_SPECIFIER);
2101
+ return hasHolds ? [...withoutBaseline, LINT_BASELINE_SPECIFIER] : withoutBaseline;
2102
+ }
2103
+ function describeHolds(holds) {
2104
+ if (holds.length === 0) return "";
2105
+ const total = holds.reduce((sum, hold) => sum + hold.count, 0);
2106
+ const listed = holds.slice(0, 5).map(({ rule, count }) => `${rule} (${count})`).join(", ");
2107
+ const rest = holds.length > 5 ? `, and ${holds.length - 5} more` : "";
2108
+ 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`;
2109
+ }
2110
+ function heldRules(cwd) {
2111
+ const path = join16(cwd, LINT_BASELINE_FILE);
2112
+ if (!existsSync13(path)) return [];
2113
+ try {
2114
+ const parsed = parseJsonc(
2115
+ readFileSync12(path, "utf8"),
2116
+ LINT_BASELINE_FILE
2117
+ );
2118
+ return Object.keys(parsed.rules ?? {});
2119
+ } catch {
2120
+ return [];
2121
+ }
2122
+ }
2123
+
1898
2124
  // src/roles/lint/config-policy.ts
1899
2125
  var LINT_CONFIG_FILE = ".oxlintrc.json";
1900
2126
  var LINT_SCRIPT_NAME = "lint";
@@ -1907,8 +2133,10 @@ function isSentinelPreset(entry) {
1907
2133
  }
1908
2134
  function extendsWithPreset(current, variant) {
1909
2135
  const own = presetPath(variant);
1910
- const others = current.filter((entry) => entry !== own && !isSentinelPreset(entry));
1911
- return [own, ...new Set(others)];
2136
+ const others = [...new Set(current.filter((entry) => entry !== own && !isSentinelPreset(entry)))];
2137
+ const baseline = others.filter((entry) => entry === LINT_BASELINE_SPECIFIER);
2138
+ const rest = others.filter((entry) => entry !== LINT_BASELINE_SPECIFIER);
2139
+ return [own, ...rest, ...baseline];
1912
2140
  }
1913
2141
  function presetVariant(preset, cwd) {
1914
2142
  const normalised = cwd.replaceAll("\\", "/");
@@ -1932,46 +2160,6 @@ function lintTarget() {
1932
2160
  return { cache: true, inputs: ["default", "^default", `{projectRoot}/${LINT_CONFIG_FILE}`] };
1933
2161
  }
1934
2162
 
1935
- // src/roles/lint/presets/base.json
1936
- var base_default2 = {
1937
- rules: {
1938
- "no-var": "error",
1939
- "prefer-const": [
1940
- "error",
1941
- {
1942
- destructuring: "any",
1943
- ignoreReadBeforeAssign: false
1944
- }
1945
- ],
1946
- "prefer-rest-params": "error",
1947
- "prefer-spread": "error",
1948
- "typescript/ban-ts-comment": "error",
1949
- "typescript/no-array-constructor": "error",
1950
- "typescript/no-duplicate-enum-values": "error",
1951
- "typescript/no-empty-object-type": "error",
1952
- "typescript/no-extra-non-null-assertion": "error",
1953
- "typescript/no-misused-new": "error",
1954
- "typescript/no-namespace": "error",
1955
- "typescript/no-non-null-asserted-optional-chain": "error",
1956
- "typescript/no-this-alias": "error",
1957
- "typescript/no-unnecessary-type-constraint": "error",
1958
- "typescript/no-unsafe-declaration-merging": "error",
1959
- "typescript/no-unsafe-function-type": "error",
1960
- "typescript/no-unused-expressions": [
1961
- "error",
1962
- {
1963
- allowShortCircuit: false,
1964
- allowTaggedTemplates: false,
1965
- allowTernary: false
1966
- }
1967
- ],
1968
- "typescript/no-wrapper-object-types": "error",
1969
- "typescript/prefer-as-const": "error",
1970
- "typescript/prefer-namespace-keyword": "error",
1971
- "typescript/triple-slash-reference": "error"
1972
- }
1973
- };
1974
-
1975
2163
  // src/roles/lint/presets/nest.json
1976
2164
  var nest_default = {
1977
2165
  plugins: ["typescript", "jest", "import", "unicorn", "oxc", "node"],
@@ -2311,6 +2499,7 @@ var nest_default = {
2311
2499
  "no-invalid-this": "no oxlint equivalent, and hosting does not help: the rule crashes on every file under oxlint's plugin API (sourceCode.getJSDocComment is not implemented). Its coverage moves to TypeScript's noImplicitThis, which the sentinel TypeScript role manages.",
2312
2500
  "no-multi-spaces": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
2313
2501
  "no-octal": "no oxlint equivalent",
2502
+ "no-restricted-syntax": "no oxlint equivalent, and unlike the react tier this preset hosts no house-rule plugin, so architectural guards written as AST selectors are not enforced at all. They belong in sentinel's `hublo` plugin, next to the frontend ones: tell us which guards your service relies on. A per-module copy of a house rule drifts, which is the problem sentinel exists to remove.",
2314
2503
  "no-trailing-spaces": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
2315
2504
  semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
2316
2505
  "template-curly-spacing": "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between.",
@@ -3264,10 +3453,51 @@ var react_default = {
3264
3453
  uncovered: {
3265
3454
  "import/order": "import ordering is FORMATTING, and the format role owns it: oxfmt's `sortImports` reproduces exactly these groups (builtin, external, internal, parent/index, sibling, alphabetical, blank line between) and sorts a file completely in ONE pass, because it rewrites the whole import block. A lint fixer cannot: it emits text edits over overlapping ranges, so this rule needed several runs to finish (five imports took four passes, measured) and sentinel deliberately runs autofix once. Enforced by `pnpm run format`, not by the linter, which is also why `eslint-plugin-import` is no longer hosted at all: its only other rule, no-duplicates, is native in oxlint.",
3266
3455
  "no-octal": "no oxlint equivalent",
3456
+ "no-restricted-syntax": "no oxlint equivalent as configuration, so the eight AST selectors this repo used are shipped as real rules in the local `hublo` plugin instead, which this tier hosts (`../plugins/hublo.js`). Any selector NOT in that plugin is not enforced here; propose it and it joins the plugin rather than a per-module copy.",
3267
3457
  semi: "formatting, and the formatter owns it. It moves to oxfmt, which is deliberately the LAST step of the migration, so nothing enforces it in between."
3268
3458
  }
3269
3459
  };
3270
3460
 
3461
+ // src/roles/lint/presets/shared.json
3462
+ var shared_default = {
3463
+ rules: {
3464
+ "no-var": "error",
3465
+ "prefer-const": [
3466
+ "error",
3467
+ {
3468
+ destructuring: "any",
3469
+ ignoreReadBeforeAssign: false
3470
+ }
3471
+ ],
3472
+ "prefer-rest-params": "error",
3473
+ "prefer-spread": "error",
3474
+ "typescript/ban-ts-comment": "error",
3475
+ "typescript/no-array-constructor": "error",
3476
+ "typescript/no-duplicate-enum-values": "error",
3477
+ "typescript/no-empty-object-type": "error",
3478
+ "typescript/no-extra-non-null-assertion": "error",
3479
+ "typescript/no-misused-new": "error",
3480
+ "typescript/no-namespace": "error",
3481
+ "typescript/no-non-null-asserted-optional-chain": "error",
3482
+ "typescript/no-this-alias": "error",
3483
+ "typescript/no-unnecessary-type-constraint": "error",
3484
+ "typescript/no-unsafe-declaration-merging": "error",
3485
+ "typescript/no-unsafe-function-type": "error",
3486
+ "typescript/no-unused-expressions": [
3487
+ "error",
3488
+ {
3489
+ allowShortCircuit: false,
3490
+ allowTaggedTemplates: false,
3491
+ allowTernary: false
3492
+ }
3493
+ ],
3494
+ "typescript/no-wrapper-object-types": "error",
3495
+ "typescript/prefer-as-const": "error",
3496
+ "typescript/prefer-namespace-keyword": "error",
3497
+ "typescript/triple-slash-reference": "error"
3498
+ }
3499
+ };
3500
+
3271
3501
  // src/roles/lint/presets/svelte.json
3272
3502
  var svelte_default = {
3273
3503
  plugins: ["typescript", "oxc"],
@@ -3606,7 +3836,7 @@ var tools_default = {
3606
3836
  };
3607
3837
 
3608
3838
  // src/roles/lint/preset-data.ts
3609
- var BASE_RULES = base_default2.rules;
3839
+ var SHARED_RULES = shared_default.rules;
3610
3840
  var PRESET_FILES = {
3611
3841
  nest: nest_default,
3612
3842
  node: node_default,
@@ -3628,15 +3858,18 @@ function hasLintPreset(preset) {
3628
3858
  function presetFile(preset) {
3629
3859
  return VARIANT_FILES[preset] ?? PRESET_FILES[preset] ?? node_default;
3630
3860
  }
3631
- function baseRules() {
3632
- return BASE_RULES;
3861
+ function sharedRules() {
3862
+ return SHARED_RULES;
3633
3863
  }
3634
3864
  function presetRules(preset) {
3635
3865
  return presetFile(preset).rules;
3636
3866
  }
3867
+ function uncoveredFor(preset) {
3868
+ return Object.entries(presetFile(preset).uncovered ?? {}).map(([rule, reason]) => ({ rule, reason })).sort((left, right) => left.rule.localeCompare(right.rule));
3869
+ }
3637
3870
 
3638
3871
  // src/roles/lint/rule-data.ts
3639
- var baseRules2 = baseRules();
3872
+ var sharedRules2 = sharedRules();
3640
3873
  function isDecision(value) {
3641
3874
  return typeof value === "object" && value !== null && !Array.isArray(value);
3642
3875
  }
@@ -3663,7 +3896,7 @@ function validate(preset, rules) {
3663
3896
  }
3664
3897
  }
3665
3898
  function layered(preset) {
3666
- const base = baseRules();
3899
+ const base = sharedRules();
3667
3900
  const layer = presetRules(preset);
3668
3901
  validate("base", base);
3669
3902
  validate(preset, layer);
@@ -3702,60 +3935,14 @@ function downgradedRulesFor(preset) {
3702
3935
  }
3703
3936
 
3704
3937
  // src/roles/lint/extra-layers.ts
3705
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
3706
- import { isAbsolute as isAbsolute2, join as join15, resolve as resolve3 } from "path";
3707
-
3708
- // src/roles/lint/module-baseline.ts
3709
- var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
3710
- var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
3711
- var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
3712
- function holdsFrom(errorCounts) {
3713
- return [...errorCounts].map(([rule, count]) => ({ rule, count })).sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule));
3714
- }
3715
- function renderBaseline(holds, version) {
3716
- const entries = holds.map(({ rule }) => ` ${JSON.stringify(rule)}: "warn"`).join(",\n");
3717
- return [
3718
- "{",
3719
- ` // Written by @hublo/sentinel@${version}. Do not edit by hand: \`sentinel --init --lint\``,
3720
- " // regenerates it from a fresh measurement, and a rule you have since fixed drops out",
3721
- " // and returns to `error` on its own.",
3722
- " //",
3723
- " // These rules are held at `warn` because this module ALREADY violated them when it",
3724
- " // adopted. They still run and still report, so the build cannot fail on them.",
3725
- " //",
3726
- " // The hold is per RULE, not per occurrence: oxlint cannot freeze a specific list of",
3727
- " // violations, so a NEW violation of one of these rules is covered too, and will only",
3728
- " // warn. That is the cost of adopting without a red build, and the reason to clear this",
3729
- " // file rather than live with it.",
3730
- " //",
3731
- " // `sentinel --inspect --lint` counts what is left. Fix them and re-run `--init --lint`",
3732
- " // to get the preset severity back.",
3733
- ' "rules": {',
3734
- entries,
3735
- " }",
3736
- "}",
3737
- ""
3738
- ].join("\n");
3739
- }
3740
- function extendsWithBaseline(current, hasHolds) {
3741
- const withoutBaseline = current.filter((entry) => entry !== LINT_BASELINE_SPECIFIER);
3742
- return hasHolds ? [...withoutBaseline, LINT_BASELINE_SPECIFIER] : withoutBaseline;
3743
- }
3744
- function describeHolds(holds) {
3745
- if (holds.length === 0) return "";
3746
- const total = holds.reduce((sum, hold) => sum + hold.count, 0);
3747
- const listed = holds.slice(0, 5).map(({ rule, count }) => `${rule} (${count})`).join(", ");
3748
- const rest = holds.length > 5 ? `, and ${holds.length - 5} more` : "";
3749
- 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`;
3750
- }
3751
-
3752
- // src/roles/lint/extra-layers.ts
3938
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
3939
+ import { isAbsolute as isAbsolute2, join as join17, resolve as resolve3 } from "path";
3753
3940
  function ruleCount(cwd, specifier) {
3754
3941
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
3755
- if (!existsSync13(path)) return void 0;
3942
+ if (!existsSync14(path)) return void 0;
3756
3943
  try {
3757
3944
  const parsed = parseJsonc(
3758
- readFileSync12(path, "utf8"),
3945
+ readFileSync13(path, "utf8"),
3759
3946
  specifier
3760
3947
  );
3761
3948
  return Object.keys(parsed.rules ?? {}).length;
@@ -3775,7 +3962,7 @@ function extraLayers(cwd, extendsList) {
3775
3962
  function committedExtendsList(cwd, configFile) {
3776
3963
  try {
3777
3964
  const parsed = parseJsonc(
3778
- readFileSync12(join15(cwd, configFile), "utf8"),
3965
+ readFileSync13(join17(cwd, configFile), "utf8"),
3779
3966
  configFile
3780
3967
  );
3781
3968
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -3834,8 +4021,8 @@ function errorCountsByConfigRule(stdout) {
3834
4021
  }
3835
4022
 
3836
4023
  // src/core/config/read-adoption.ts
3837
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
3838
- import { join as join17 } from "path";
4024
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4025
+ import { join as join19 } from "path";
3839
4026
 
3840
4027
  // src/core/config/owned-keys.ts
3841
4028
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -3850,12 +4037,12 @@ function localOnlyKeys(config, permitted, presetSets) {
3850
4037
  }
3851
4038
 
3852
4039
  // src/core/config/resolve-config-target.ts
3853
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
3854
- import { join as join16 } from "path";
4040
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4041
+ import { join as join18 } from "path";
3855
4042
  function readExtends(absolutePath) {
3856
4043
  let parsed;
3857
4044
  try {
3858
- parsed = parseJsonc(readFileSync13(absolutePath, "utf8"), absolutePath);
4045
+ parsed = parseJsonc(readFileSync14(absolutePath, "utf8"), absolutePath);
3859
4046
  } catch {
3860
4047
  return [];
3861
4048
  }
@@ -3869,8 +4056,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
3869
4056
  let existing;
3870
4057
  let existingExtendsSomething = false;
3871
4058
  for (const candidate of candidates) {
3872
- const absolutePath = join16(moduleDir, candidate);
3873
- if (!existsSync14(absolutePath)) continue;
4059
+ const absolutePath = join18(moduleDir, candidate);
4060
+ if (!existsSync15(absolutePath)) continue;
3874
4061
  const chain = readExtends(absolutePath);
3875
4062
  if (existing === void 0) {
3876
4063
  existing = candidate;
@@ -3903,12 +4090,12 @@ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
3903
4090
  });
3904
4091
  function readAdoption(cwd, options) {
3905
4092
  const target = resolveConfigTarget(cwd, options);
3906
- if (target.reason === "none" || !existsSync15(join17(cwd, target.path))) {
4093
+ if (target.reason === "none" || !existsSync16(join19(cwd, target.path))) {
3907
4094
  return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
3908
4095
  }
3909
4096
  let parsed;
3910
4097
  try {
3911
- parsed = parseJsonc(readFileSync14(join17(cwd, target.path), "utf8"), target.path);
4098
+ parsed = parseJsonc(readFileSync15(join19(cwd, target.path), "utf8"), target.path);
3912
4099
  } catch (error) {
3913
4100
  const reason = error instanceof Error ? error.message : String(error);
3914
4101
  return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
@@ -3942,9 +4129,9 @@ function readLintAdoption(cwd) {
3942
4129
  }
3943
4130
 
3944
4131
  // src/roles/lint/resolve-oxlint.ts
3945
- import { existsSync as existsSync16 } from "fs";
4132
+ import { existsSync as existsSync17 } from "fs";
3946
4133
  import { createRequire as createRequire3 } from "module";
3947
- import { delimiter as delimiter2, dirname as dirname5, join as join18 } from "path";
4134
+ import { delimiter as delimiter2, dirname as dirname5, join as join20 } from "path";
3948
4135
  import { fileURLToPath as fileURLToPath2 } from "url";
3949
4136
  var PACKAGE_OF = {
3950
4137
  oxlint: "oxlint",
@@ -3969,30 +4156,30 @@ function tsgolintShim(cwd) {
3969
4156
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
3970
4157
  try {
3971
4158
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
3972
- candidates.push(join18(packageDir, "node_modules", ".bin", "tsgolint"));
3973
- candidates.push(join18(packageDir, "..", ".bin", "tsgolint"));
4159
+ candidates.push(join20(packageDir, "node_modules", ".bin", "tsgolint"));
4160
+ candidates.push(join20(packageDir, "..", ".bin", "tsgolint"));
3974
4161
  } catch {
3975
4162
  }
3976
4163
  }
3977
4164
  try {
3978
4165
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
3979
- candidates.push(join18(ownRoot, "node_modules", ".bin", "tsgolint"));
4166
+ candidates.push(join20(ownRoot, "node_modules", ".bin", "tsgolint"));
3980
4167
  } catch {
3981
4168
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
3982
4169
  }
3983
- return candidates.find((candidate) => candidate !== "" && existsSync16(candidate));
4170
+ return candidates.find((candidate) => candidate !== "" && existsSync17(candidate));
3984
4171
  }
3985
4172
  function oxlintSearchPath(cwd) {
3986
4173
  return binSearchPath(cwd);
3987
4174
  }
3988
4175
 
3989
4176
  // src/roles/lint/adapters/oxlint/plan.ts
3990
- import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
3991
- import { join as join21 } from "path";
4177
+ import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
4178
+ import { join as join23 } from "path";
3992
4179
 
3993
4180
  // src/roles/lint/eslint-ignores.ts
3994
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
3995
- import { join as join19 } from "path";
4181
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4182
+ import { join as join21 } from "path";
3996
4183
  var IGNORE_BLOCKS = [/\bignores\s*:\s*\[([^\]]*)\]/g, /\bglobalIgnores\s*\(\s*\[([^\]]*)\]/g];
3997
4184
  var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
3998
4185
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
@@ -4005,11 +4192,11 @@ function readRootEslintIgnores(root) {
4005
4192
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
4006
4193
  }
4007
4194
  function readEslintIgnores(cwd) {
4008
- const config = ESLINT_CONFIG_FILES.map((name) => join19(cwd, name)).find((path) => existsSync17(path));
4195
+ const config = ESLINT_CONFIG_FILES.map((name) => join21(cwd, name)).find((path) => existsSync18(path));
4009
4196
  if (!config) return { patterns: [], unresolved: [] };
4010
4197
  let source;
4011
4198
  try {
4012
- source = readFileSync15(config, "utf8");
4199
+ source = readFileSync16(config, "utf8");
4013
4200
  } catch {
4014
4201
  return { patterns: [], unresolved: [] };
4015
4202
  }
@@ -4063,8 +4250,8 @@ function lintPresetFor(preset) {
4063
4250
  }
4064
4251
 
4065
4252
  // src/roles/lint/rename-suppressions.ts
4066
- import { readdirSync as readdirSync2, readFileSync as readFileSync16, statSync } from "fs";
4067
- import { join as join20, relative as relative2 } from "path";
4253
+ import { readdirSync as readdirSync2, readFileSync as readFileSync17, statSync as statSync2 } from "fs";
4254
+ import { join as join22, relative as relative3 } from "path";
4068
4255
  var SOURCE_EXTENSIONS = [
4069
4256
  ".ts",
4070
4257
  ".tsx",
@@ -4111,10 +4298,10 @@ function* sourceFiles(dir) {
4111
4298
  return;
4112
4299
  }
4113
4300
  for (const entry of entries) {
4114
- const full = join20(dir, entry);
4301
+ const full = join22(dir, entry);
4115
4302
  let isDirectory;
4116
4303
  try {
4117
- isDirectory = statSync(full).isDirectory();
4304
+ isDirectory = statSync2(full).isDirectory();
4118
4305
  } catch {
4119
4306
  continue;
4120
4307
  }
@@ -4131,7 +4318,7 @@ function findSuppressionRenames(cwd, renames) {
4131
4318
  for (const file of sourceFiles(cwd)) {
4132
4319
  let content;
4133
4320
  try {
4134
- content = readFileSync16(file, "utf8");
4321
+ content = readFileSync17(file, "utf8");
4135
4322
  } catch {
4136
4323
  continue;
4137
4324
  }
@@ -4155,7 +4342,7 @@ function findSuppressionRenames(cwd, renames) {
4155
4342
  }
4156
4343
  if (hits.length === 0) return;
4157
4344
  found.push({
4158
- file: relative2(cwd, file),
4345
+ file: relative3(cwd, file),
4159
4346
  line: index + 1,
4160
4347
  from: line,
4161
4348
  to: line.replace(rules, renamed.join(", ")),
@@ -4259,7 +4446,7 @@ function plan2(context) {
4259
4446
  keys: removableDeps
4260
4447
  });
4261
4448
  }
4262
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync18(join21(context.cwd, name)));
4449
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync19(join23(context.cwd, name)));
4263
4450
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
4264
4451
  operations.push(
4265
4452
  ...nxTargetOperations({
@@ -4357,7 +4544,7 @@ function lintScripts(cwd) {
4357
4544
  function committedExtends(cwd) {
4358
4545
  try {
4359
4546
  const parsed = parseJsonc(
4360
- readFileSync17(join21(cwd, LINT_CONFIG_FILE), "utf8"),
4547
+ readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
4361
4548
  LINT_CONFIG_FILE
4362
4549
  );
4363
4550
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -4368,7 +4555,7 @@ function committedExtends(cwd) {
4368
4555
  function committedIgnorePatterns(cwd) {
4369
4556
  try {
4370
4557
  const parsed = parseJsonc(
4371
- readFileSync17(join21(cwd, LINT_CONFIG_FILE), "utf8"),
4558
+ readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
4372
4559
  LINT_CONFIG_FILE
4373
4560
  );
4374
4561
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -4380,7 +4567,7 @@ function moduleEslintDependencies(cwd) {
4380
4567
  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/");
4381
4568
  let manifest;
4382
4569
  try {
4383
- manifest = JSON.parse(readFileSync17(join21(cwd, "package.json"), "utf8"));
4570
+ manifest = JSON.parse(readFileSync18(join23(cwd, "package.json"), "utf8"));
4384
4571
  } catch {
4385
4572
  return [];
4386
4573
  }
@@ -4445,17 +4632,31 @@ var OxlintAdapter = class extends BaseAdapter {
4445
4632
  return presetOfVariant(presetNameFromPath(readLintAdoption(cwd).preset));
4446
4633
  }
4447
4634
  /**
4448
- * The autofix pass. Output is captured rather than streamed: the fix run is noise, and the
4449
- * verification run that follows is the one a developer reads.
4635
+ * The autofix pass, and what it did.
4636
+ *
4637
+ * Output is captured rather than streamed: the fix run is noise, and the verification run
4638
+ * that follows is the one a developer reads. But captured is not the same as discarded, and
4639
+ * it used to be discarded, which is why a pass that never ran looked exactly like one that
4640
+ * had nothing to do.
4641
+ *
4642
+ * The exit status cannot be the signal, and neither can the stream. Measured on oxlint: a
4643
+ * healthy run on a module with remaining violations and a run whose `extends` path resolves
4644
+ * nowhere BOTH exit 1 and both write to stdout. One module was checked and one was not, and
4645
+ * nothing in the process result separates them, so the only signal is what the tool said.
4450
4646
  */
4451
4647
  fixPass(ctx, oxlint, env) {
4452
- const result = spawnSync5(oxlint, ["-c", LINT_CONFIG_FILE, "--fix", "."], {
4648
+ const before = snapshotFiles(ctx.cwd, LINTABLE_EXTENSIONS);
4649
+ const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, "--fix", "."], {
4453
4650
  cwd: ctx.cwd,
4454
4651
  encoding: "utf8",
4455
4652
  env,
4456
4653
  maxBuffer: CAPTURE_MAX_BUFFER
4457
4654
  });
4458
- return result.status ?? 1;
4655
+ if (result.error) return { changed: [], failure: result.error.message };
4656
+ const failure = readFailure(`${result.stdout ?? ""}
4657
+ ${result.stderr ?? ""}`);
4658
+ if (failure !== void 0) return { changed: [], failure };
4659
+ return { changed: changedSince(ctx.cwd, before, LINTABLE_EXTENSIONS), failure: void 0 };
4459
4660
  }
4460
4661
  /** The module's current `lint` script, so adoption replaces only the eslint part of it. */
4461
4662
  /**
@@ -4478,7 +4679,8 @@ var OxlintAdapter = class extends BaseAdapter {
4478
4679
  const oxlint = resolveOxlint(ctx.cwd);
4479
4680
  if (!oxlint) return;
4480
4681
  const env = { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) };
4481
- this.fixPass(ctx, oxlint, env);
4682
+ process.stderr.write(` ${describeFixOutcome(this.fixPass(ctx, oxlint, env), "lint")}
4683
+ `);
4482
4684
  this.writeModuleBaseline(ctx, oxlint, env);
4483
4685
  }
4484
4686
  /**
@@ -4499,20 +4701,20 @@ var OxlintAdapter = class extends BaseAdapter {
4499
4701
  * build the developer can see, rather than a silent half-adoption they cannot.
4500
4702
  */
4501
4703
  writeModuleBaseline(ctx, oxlint, env) {
4502
- const configPath = join22(ctx.cwd, LINT_CONFIG_FILE);
4503
- const baselinePath = join22(ctx.cwd, LINT_BASELINE_FILE);
4504
- if (!existsSync19(configPath)) return;
4704
+ const configPath = join24(ctx.cwd, LINT_CONFIG_FILE);
4705
+ const baselinePath = join24(ctx.cwd, LINT_BASELINE_FILE);
4706
+ if (!existsSync20(configPath)) return;
4505
4707
  let config;
4506
4708
  try {
4507
4709
  config = parseJsonc(
4508
- readFileSync18(configPath, "utf8"),
4710
+ readFileSync19(configPath, "utf8"),
4509
4711
  LINT_CONFIG_FILE
4510
4712
  );
4511
4713
  } catch {
4512
4714
  return;
4513
4715
  }
4514
4716
  const current = Array.isArray(config.extends) ? config.extends : [];
4515
- const measurePath = join22(ctx.cwd, LINT_MEASURE_FILE);
4717
+ const measurePath = join24(ctx.cwd, LINT_MEASURE_FILE);
4516
4718
  let measured;
4517
4719
  try {
4518
4720
  writeFileSync2(
@@ -4520,7 +4722,7 @@ var OxlintAdapter = class extends BaseAdapter {
4520
4722
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
4521
4723
  `
4522
4724
  );
4523
- measured = spawnSync5(
4725
+ measured = spawnSync4(
4524
4726
  oxlint,
4525
4727
  [
4526
4728
  "-c",
@@ -4553,14 +4755,14 @@ var OxlintAdapter = class extends BaseAdapter {
4553
4755
  } else {
4554
4756
  rmSync(baselinePath, { force: true });
4555
4757
  }
4556
- if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
4758
+ if (next.length !== current.length || next.some((entry, at2) => entry !== current[at2])) {
4557
4759
  config.extends = next;
4558
4760
  writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
4559
4761
  `);
4560
4762
  }
4561
4763
  }
4562
4764
  async run(ctx) {
4563
- if (!existsSync19(join22(ctx.cwd, LINT_CONFIG_FILE))) {
4765
+ if (!existsSync20(join24(ctx.cwd, LINT_CONFIG_FILE))) {
4564
4766
  process.stderr.write(
4565
4767
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
4566
4768
  `
@@ -4605,9 +4807,10 @@ var OxlintAdapter = class extends BaseAdapter {
4605
4807
  valueFlags: OXLINT_VALUE_FLAGS
4606
4808
  });
4607
4809
  const targets = paths.length > 0 ? paths : ["."];
4810
+ const emptyScope = scopeFlags(paths);
4608
4811
  const lint = (extra) => {
4609
- const passed = [...extra, ...passedOptions];
4610
- const result = spawnSync5(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
4812
+ const passed = [...extra, ...emptyScope, ...passedOptions];
4813
+ const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
4611
4814
  cwd: ctx.cwd,
4612
4815
  stdio: "inherit",
4613
4816
  env
@@ -4645,6 +4848,33 @@ var OxlintAdapter = class extends BaseAdapter {
4645
4848
  rule: entry.rule,
4646
4849
  reason: entry.reason
4647
4850
  })),
4851
+ // A FOURTH state, and the only one that was declared and never reported: rules oxlint
4852
+ // cannot run at all, so they can be neither enforced nor disabled. Each preset has
4853
+ // carried this list since the Svelte work, and nothing read it outside a test.
4854
+ //
4855
+ // The gap that made it matter: `no-restricted-syntax` has no oxlint equivalent, so two
4856
+ // architectural guards a Nest service relied on vanished at adoption with nothing said.
4857
+ // The adopter found it himself and rebuilt them as a local plugin. The next one might
4858
+ // not, which is the whole argument for saying it out loud.
4859
+ uncovered: uncoveredFor(presetVariant(ctx.preset, ctx.cwd)).map((entry) => ({
4860
+ rule: entry.rule,
4861
+ reason: entry.reason
4862
+ })),
4863
+ // What this module HOLDS: the rules its own baseline lowers to `warn`.
4864
+ //
4865
+ // Distinct from `downgraded` just above, which is the PRESET's doing and identical for
4866
+ // every module on it. These are this module's own debt, and leaving them out was a real
4867
+ // gap: `--inspect` could tell you the preset holds 6 rules while saying nothing about
4868
+ // the 3 your module holds on top, so nobody could see their own debt without opening a
4869
+ // file that says "do not edit by hand".
4870
+ //
4871
+ // Listed, not counted. The list is free — it is the committed file — while a count means
4872
+ // running oxlint, which this verb deliberately does not do (that is what keeps a
4873
+ // 441-module sweep at ~8s per role). `--run --lint --json` carries the per-rule counts.
4874
+ held: heldRules(ctx.cwd).map((rule) => ({
4875
+ rule,
4876
+ reason: `held at \`warn\` by this module's ${LINT_BASELINE_FILE}, because it already violated it when it adopted. \`--run --lint --json\` counts what is left.`
4877
+ })),
4648
4878
  // What this module enforces BEYOND the preset. Not drift, and not covered by anything
4649
4879
  // else here: the stub still holds only `extends` and `ignorePatterns`, so a module with
4650
4880
  // a team layer reports `conformant=true drift=[]` and used to say nothing at all about
@@ -4680,7 +4910,7 @@ var OxlintAdapter = class extends BaseAdapter {
4680
4910
  let stub;
4681
4911
  try {
4682
4912
  stub = parseJsonc(
4683
- readFileSync18(join22(cwd, LINT_CONFIG_FILE), "utf8"),
4913
+ readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
4684
4914
  LINT_CONFIG_FILE
4685
4915
  );
4686
4916
  } catch {
@@ -4690,7 +4920,7 @@ var OxlintAdapter = class extends BaseAdapter {
4690
4920
  for (const entry of stub.extends ?? []) {
4691
4921
  try {
4692
4922
  const preset = parseJsonc(
4693
- readFileSync18(join22(cwd, entry), "utf8"),
4923
+ readFileSync19(join24(cwd, entry), "utf8"),
4694
4924
  entry
4695
4925
  );
4696
4926
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -4707,7 +4937,7 @@ var OxlintAdapter = class extends BaseAdapter {
4707
4937
  const oxlint = resolveOxlint(ctx.cwd);
4708
4938
  if (!oxlint) return { ok: false, code: 1, metrics: { error: "oxlint not found" } };
4709
4939
  const typeAware = canRunTypeAware(ctx.cwd) ? ["--type-aware"] : [];
4710
- const result = spawnSync5(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
4940
+ const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
4711
4941
  cwd: ctx.cwd,
4712
4942
  encoding: "utf8",
4713
4943
  env: { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) },
@@ -4760,14 +4990,14 @@ var OxlintAdapter = class extends BaseAdapter {
4760
4990
  let parsed;
4761
4991
  try {
4762
4992
  parsed = parseJsonc(
4763
- readFileSync18(join22(cwd, LINT_CONFIG_FILE), "utf8"),
4993
+ readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
4764
4994
  LINT_CONFIG_FILE
4765
4995
  );
4766
4996
  } catch {
4767
4997
  return void 0;
4768
4998
  }
4769
4999
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
4770
- return targets.find((target) => !existsSync19(join22(cwd, target)));
5000
+ return targets.find((target) => !existsSync20(join24(cwd, target)));
4771
5001
  }
4772
5002
  /** Announce what is not enforced, so reduced coverage is never silent. */
4773
5003
  announceDisabled(preset) {
@@ -4798,36 +5028,10 @@ function registerLint() {
4798
5028
  }
4799
5029
 
4800
5030
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
4801
- import { spawnSync as spawnSync6 } from "child_process";
4802
- import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
5031
+ import { spawnSync as spawnSync5 } from "child_process";
5032
+ import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
4803
5033
  import { createRequire as createRequire4 } from "module";
4804
- import { join as join24 } from "path";
4805
-
4806
- // src/roles/typescript/presets/base.json
4807
- var base_default3 = {
4808
- $schema: "https://json.schemastore.org/tsconfig",
4809
- compilerOptions: {
4810
- strict: true,
4811
- noFallthroughCasesInSwitch: true,
4812
- forceConsistentCasingInFileNames: true,
4813
- esModuleInterop: true,
4814
- skipLibCheck: true
4815
- },
4816
- deferred: {
4817
- noImplicitAny: {
4818
- phase: 2,
4819
- reason: "the implicit-any migration (TS70xx)"
4820
- },
4821
- noUnusedLocals: {
4822
- phase: 2,
4823
- reason: "unused-local cleanup (TS6133)"
4824
- },
4825
- noUnusedParameters: {
4826
- phase: 2,
4827
- reason: "unused-parameter cleanup (TS6133)"
4828
- }
4829
- }
4830
- };
5034
+ import { join as join26 } from "path";
4831
5035
 
4832
5036
  // src/roles/typescript/presets/nest.json
4833
5037
  var nest_default2 = {
@@ -4878,15 +5082,41 @@ var react_default2 = {
4878
5082
  }
4879
5083
  };
4880
5084
 
5085
+ // src/roles/typescript/presets/shared.json
5086
+ var shared_default2 = {
5087
+ $schema: "https://json.schemastore.org/tsconfig",
5088
+ compilerOptions: {
5089
+ strict: true,
5090
+ noFallthroughCasesInSwitch: true,
5091
+ forceConsistentCasingInFileNames: true,
5092
+ esModuleInterop: true,
5093
+ skipLibCheck: true
5094
+ },
5095
+ deferred: {
5096
+ noImplicitAny: {
5097
+ phase: 2,
5098
+ reason: "the implicit-any migration (TS70xx)"
5099
+ },
5100
+ noUnusedLocals: {
5101
+ phase: 2,
5102
+ reason: "unused-local cleanup (TS6133)"
5103
+ },
5104
+ noUnusedParameters: {
5105
+ phase: 2,
5106
+ reason: "unused-parameter cleanup (TS6133)"
5107
+ }
5108
+ }
5109
+ };
5110
+
4881
5111
  // src/roles/typescript/preset-data.ts
4882
- var BASE = base_default3;
5112
+ var SHARED = shared_default2;
4883
5113
  var LAYERS = {
4884
5114
  nest: nest_default2,
4885
5115
  node: node_default2,
4886
5116
  react: react_default2
4887
5117
  };
4888
5118
  var SHIPPED_PRESETS = Object.keys(LAYERS).sort();
4889
- var DEFERRED_RULES = Object.entries(BASE.deferred ?? {}).map(([rule, entry]) => ({ rule, phase: entry.phase, reason: entry.reason })).sort((left, right) => left.rule.localeCompare(right.rule));
5119
+ var DEFERRED_RULES = Object.entries(SHARED.deferred ?? {}).map(([rule, entry]) => ({ rule, phase: entry.phase, reason: entry.reason })).sort((left, right) => left.rule.localeCompare(right.rule));
4890
5120
  function validate2() {
4891
5121
  for (const { rule, phase, reason } of DEFERRED_RULES) {
4892
5122
  if (!Number.isInteger(phase) || phase < 2) {
@@ -4899,9 +5129,9 @@ function validate2() {
4899
5129
  `deferred rule "${rule}" has no reason; --inspect reports it and would say nothing`
4900
5130
  );
4901
5131
  }
4902
- if (rule in BASE.compilerOptions) {
5132
+ if (rule in SHARED.compilerOptions) {
4903
5133
  throw new Error(
4904
- `"${rule}" is deferred and also set by the base preset; it would be announced as not enforced while being enforced`
5134
+ `"${rule}" is deferred and also set by the shared layer; it would be announced as not enforced while being enforced`
4905
5135
  );
4906
5136
  }
4907
5137
  for (const [preset, layer] of Object.entries(LAYERS)) {
@@ -4921,8 +5151,8 @@ function tsPresetFor(preset) {
4921
5151
  const layer = LAYERS[preset];
4922
5152
  if (!layer) throw new Error(`no TypeScript preset ships for preset "${preset}"`);
4923
5153
  return {
4924
- ...BASE.$schema === void 0 ? {} : { $schema: BASE.$schema },
4925
- compilerOptions: { ...BASE.compilerOptions, ...layer.compilerOptions }
5154
+ ...SHARED.$schema === void 0 ? {} : { $schema: SHARED.$schema },
5155
+ compilerOptions: { ...SHARED.compilerOptions, ...layer.compilerOptions }
4926
5156
  };
4927
5157
  }
4928
5158
 
@@ -4982,8 +5212,8 @@ function readTsconfigAdoption(cwd) {
4982
5212
  }
4983
5213
 
4984
5214
  // src/roles/typescript/adapters/tsc/plan.ts
4985
- import { readFileSync as readFileSync19 } from "fs";
4986
- import { join as join23 } from "path";
5215
+ import { readFileSync as readFileSync20 } from "fs";
5216
+ import { join as join25 } from "path";
4987
5217
 
4988
5218
  // src/roles/typescript/typecheck-script.ts
4989
5219
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -5076,7 +5306,7 @@ function planAdoption(context) {
5076
5306
  };
5077
5307
  }
5078
5308
  const existing = parseJsonc(
5079
- readFileSync19(join23(context.cwd, target.path), "utf8"),
5309
+ readFileSync20(join25(context.cwd, target.path), "utf8"),
5080
5310
  target.path
5081
5311
  );
5082
5312
  const extendsChain = composeExtends(existing.extends, preset);
@@ -5214,7 +5444,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5214
5444
  let chain;
5215
5445
  try {
5216
5446
  const parsed = parseJsonc(
5217
- readFileSync20(join24(cwd, target.path), "utf8"),
5447
+ readFileSync21(join26(cwd, target.path), "utf8"),
5218
5448
  target.path
5219
5449
  );
5220
5450
  chain = parsed.extends;
@@ -5227,7 +5457,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5227
5457
  );
5228
5458
  if (preset === void 0) return void 0;
5229
5459
  try {
5230
- createRequire4(join24(cwd, "noop.js")).resolve(preset);
5460
+ createRequire4(join26(cwd, "noop.js")).resolve(preset);
5231
5461
  return void 0;
5232
5462
  } catch {
5233
5463
  return preset;
@@ -5268,7 +5498,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5268
5498
  return { ok: false, code: 1 };
5269
5499
  }
5270
5500
  if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
5271
- const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
5501
+ const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
5272
5502
  if (result.error) {
5273
5503
  process.stderr.write(
5274
5504
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5303,7 +5533,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5303
5533
  let worst = 0;
5304
5534
  for (const project of this.referencedProjects(ctx.cwd, config)) {
5305
5535
  const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
5306
- const result = spawnSync6(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
5536
+ const result = spawnSync5(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
5307
5537
  if (result.error) {
5308
5538
  process.stderr.write(
5309
5539
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5323,7 +5553,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5323
5553
  referencedProjects(cwd, config) {
5324
5554
  try {
5325
5555
  const parsed = parseJsonc(
5326
- readFileSync20(join24(cwd, config), "utf8"),
5556
+ readFileSync21(join26(cwd, config), "utf8"),
5327
5557
  config
5328
5558
  );
5329
5559
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -5339,7 +5569,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5339
5569
  * check.
5340
5570
  */
5341
5571
  typecheckTarget(cwd) {
5342
- if (existsSync20(join24(cwd, "tsconfig.json"))) return "tsconfig.json";
5572
+ if (existsSync21(join26(cwd, "tsconfig.json"))) return "tsconfig.json";
5343
5573
  const target = resolveTsconfigTarget(cwd);
5344
5574
  return target.reason === "none" ? null : target.path;
5345
5575
  }
@@ -5392,7 +5622,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5392
5622
  return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
5393
5623
  }
5394
5624
  const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
5395
- const result = spawnSync6(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
5625
+ const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
5396
5626
  if (result.error) {
5397
5627
  process.stderr.write(
5398
5628
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -5435,7 +5665,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
5435
5665
  * errors in the output mean the rule must be on.
5436
5666
  */
5437
5667
  noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
5438
- const shown = spawnSync6(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
5668
+ const shown = spawnSync5(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
5439
5669
  if (shown.status === 0 && shown.stdout) {
5440
5670
  try {
5441
5671
  const co = parseJsonc(
@@ -5543,7 +5773,7 @@ function replaceLines(current, replacements) {
5543
5773
  }
5544
5774
 
5545
5775
  // src/core/apply-plan.ts
5546
- import { existsSync as existsSync21, readFileSync as readFileSync21, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5776
+ import { existsSync as existsSync22, readFileSync as readFileSync22, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5547
5777
  import { resolve as resolve4, sep } from "path";
5548
5778
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
5549
5779
  function resolveWithinRoot(cwd, relativePath) {
@@ -5555,7 +5785,7 @@ function resolveWithinRoot(cwd, relativePath) {
5555
5785
  return absolutePath;
5556
5786
  }
5557
5787
  function readIfExists(absolutePath) {
5558
- return existsSync21(absolutePath) ? readFileSync21(absolutePath, "utf8") : void 0;
5788
+ return existsSync22(absolutePath) ? readFileSync22(absolutePath, "utf8") : void 0;
5559
5789
  }
5560
5790
  function* leaves(value, prefix = []) {
5561
5791
  for (const [key, keyValue] of Object.entries(value)) {
@@ -5673,8 +5903,8 @@ function applyPlan(cwd, plan3) {
5673
5903
  }
5674
5904
 
5675
5905
  // src/core/config/preset-evidence.ts
5676
- import { existsSync as existsSync22, readdirSync as readdirSync3, readFileSync as readFileSync22 } from "fs";
5677
- import { join as join25 } from "path";
5906
+ import { existsSync as existsSync23, readdirSync as readdirSync3, readFileSync as readFileSync23 } from "fs";
5907
+ import { join as join27 } from "path";
5678
5908
  var PATH_SIGNALS = [
5679
5909
  {
5680
5910
  preset: "nest",
@@ -5688,11 +5918,11 @@ var DEPENDENCY_SIGNALS = [
5688
5918
  { preset: "nest", pattern: /^@nestjs\// }
5689
5919
  ];
5690
5920
  function dependencyNames(cwd) {
5691
- const path = join25(cwd, "package.json");
5692
- if (!existsSync22(path)) return [];
5921
+ const path = join27(cwd, "package.json");
5922
+ if (!existsSync23(path)) return [];
5693
5923
  try {
5694
5924
  const manifest = parseJsonc(
5695
- readFileSync22(path, "utf8"),
5925
+ readFileSync23(path, "utf8"),
5696
5926
  path
5697
5927
  );
5698
5928
  return [
@@ -5715,7 +5945,7 @@ function declaresJsx(cwd) {
5715
5945
  for (const name of entries) {
5716
5946
  try {
5717
5947
  const config = parseJsonc(
5718
- readFileSync22(join25(cwd, name), "utf8"),
5948
+ readFileSync23(join27(cwd, name), "utf8"),
5719
5949
  name
5720
5950
  );
5721
5951
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -5812,13 +6042,26 @@ async function dispatch(opts) {
5812
6042
  const resolutionPreset = opts.preset ?? declaredPresetFor(opts.target, opts.cwd) ?? detected;
5813
6043
  const adapter = resolve(opts.target, resolutionPreset, opts.runner);
5814
6044
  const preset = opts.preset ?? adapter.declaredPreset?.(opts.cwd) ?? detected;
5815
- const contradiction = presetContradiction(preset, opts.cwd);
5816
- if (contradiction !== void 0) {
5817
- process.stderr.write(`sentinel (${opts.target}): ${contradiction}
6045
+ if (opts.preset !== void 0) {
6046
+ const contradiction = presetContradiction(opts.preset, opts.cwd);
6047
+ if (contradiction !== void 0) {
6048
+ process.stderr.write(`sentinel (${opts.target}): ${contradiction}
5818
6049
  `);
5819
- return 1;
6050
+ return 1;
6051
+ }
5820
6052
  }
5821
- const context = { cwd: opts.cwd, preset };
6053
+ let effective = preset;
6054
+ if (opts.preset === void 0 && adapter.declaredPreset?.(opts.cwd) === void 0) {
6055
+ const [signal] = presetSignals(opts.cwd);
6056
+ if (signal !== void 0 && signal.preset !== preset) {
6057
+ effective = signal.preset;
6058
+ process.stderr.write(
6059
+ ` detected "${preset}" from dependencies, but ${signal.evidence}, so adopting as "${signal.preset}". Pass --preset to override.
6060
+ `
6061
+ );
6062
+ }
6063
+ }
6064
+ const context = { cwd: opts.cwd, preset: effective };
5822
6065
  const plan3 = await adapter.plan(context);
5823
6066
  if (plan3.blocked) {
5824
6067
  process.stderr.write(`sentinel (${opts.target}): ${plan3.blocked}
@@ -5866,18 +6109,18 @@ export {
5866
6109
  readOwnVersion,
5867
6110
  readProjectPackageJson,
5868
6111
  readNxProjectName,
6112
+ VERBS,
6113
+ TARGETS,
6114
+ SWEEPABLE_TARGETS,
6115
+ PRESET_NAMES,
5869
6116
  WORKSPACE_ROOT_MARKER,
5870
6117
  findWorkspaceRoot,
5871
6118
  ensureWorkspacePrep,
5872
6119
  inspectWorkspacePrep,
5873
6120
  palette,
5874
- VERBS,
5875
- TARGETS,
5876
- SWEEPABLE_TARGETS,
5877
- PRESET_NAMES,
5878
6121
  registerAdapters,
5879
6122
  describeFramework,
5880
6123
  detectFramework,
5881
6124
  dispatch
5882
6125
  };
5883
- //# sourceMappingURL=chunk-TWL6T237.js.map
6126
+ //# sourceMappingURL=chunk-FLJ2QNFF.js.map