@hublo/sentinel 1.1.0-alpha.5 → 1.1.0-alpha.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.
package/README.md CHANGED
@@ -158,11 +158,11 @@ pnpm dlx @hublo/sentinel@<exact-version> --inspect --typescript --module <name>
158
158
 
159
159
  Every check is described by three layers:
160
160
 
161
- | Layer | Flag | What it is | Examples |
162
- | -------------------- | --------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------- |
163
- | **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable | `lint` `format` `typescript` `build` `test` `static-analysis` `runtime-analysis` |
164
- | **runner** (adapter) | `--runner=<tool>` | the _tool_ behind the target, swappable | lint: `eslint`/`biome`/`oxlint` · types: `tsc`/`tsgo` · build: `vite` · test: `vitest` |
165
- | **preset** (preset) | detected / `--preset` | the _variant_ per stack (strict by default) | `react` `nest` `node` `svelte` (svelte: lint only, no tsconfig preset yet) |
161
+ | Layer | Flag | What it is | Examples |
162
+ | -------------------- | --------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
163
+ | **target** (role) | `--lint`, `--typescript`, … | the _kind_ of check, stable | `lint` `format` `typescript` `build` `test` `static-analysis` `runtime-analysis` |
164
+ | **runner** (adapter) | `--runner=<tool>` | the _tool_ behind the target, swappable | lint: `eslint`/`biome`/`oxlint` · types: `tsc`/`tsgo` · build: `vite` · test: `vitest` |
165
+ | **preset** (preset) | detected / `--preset` | the _variant_ per stack (strict by default) | `react` `nest` `node` `svelte` (svelte: lint + format; no tsconfig preset yet, so the TypeScript role stands aside on those modules and the other two adopt) |
166
166
 
167
167
  A run is `target × runner × preset`, e.g. `sentinel --run --lint --runner=eslint` from the `host-admin` dir.
168
168
 
@@ -16,7 +16,7 @@ import {
16
16
  registerAdapters,
17
17
  resolve,
18
18
  resolveBin
19
- } from "../chunk-NAUACDXT.js";
19
+ } from "../chunk-U2NAKQXR.js";
20
20
 
21
21
  // bin/sentinel.ts
22
22
  import { program } from "commander";
@@ -140,8 +140,8 @@ 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 readFileSync7 } from "fs";
144
- import { join as join10 } from "path";
143
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
144
+ import { join as join11 } from "path";
145
145
 
146
146
  // src/core/config/has-source.ts
147
147
  import { readdirSync } from "fs";
@@ -548,6 +548,12 @@ function invokesFormat(script) {
548
548
  function keepsOtherCommands2(script, command) {
549
549
  return keepsOtherCommands(script, command ?? SENTINEL_FORMAT_COMMAND);
550
550
  }
551
+ function writesWhenRewritten(name, command) {
552
+ if (/[:-](fix|write)$/.test(name)) return true;
553
+ if (CHECK_SCRIPT_NAMES.has(name) || /[:-]check$/.test(name)) return false;
554
+ return /--write\b/.test(command);
555
+ }
556
+ var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
551
557
 
552
558
  // src/roles/format/inherited-ignores.ts
553
559
  import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
@@ -578,6 +584,42 @@ function inheritedIgnorePatterns(workspaceRoot) {
578
584
  return [...new Set(carried)].sort();
579
585
  }
580
586
 
587
+ // src/roles/format/oxfmt-diagnostics.ts
588
+ var MESSAGE = /^\s*[x\u00d7]\s+(.+?)\s*$/;
589
+ var LOCATION = /[,\u256d][-\u2500]\[([^\]]+?):(\d+):\d+\]/;
590
+ var ANSI = /\u001b\[[0-9;]*m/g;
591
+ function plain(output) {
592
+ return output.replace(ANSI, "");
593
+ }
594
+ function unparseableFiles(output) {
595
+ const diagnostics = [];
596
+ let pending;
597
+ for (const raw of plain(output).split("\n")) {
598
+ const location = LOCATION.exec(raw);
599
+ if (location && pending) {
600
+ diagnostics.push({
601
+ file: location[1] ?? "",
602
+ line: Number(location[2] ?? 0),
603
+ message: pending
604
+ });
605
+ pending = void 0;
606
+ continue;
607
+ }
608
+ const message = MESSAGE.exec(raw);
609
+ if (message) pending = message[1];
610
+ }
611
+ return diagnostics;
612
+ }
613
+ function oxfmtSaid(output) {
614
+ const lines = plain(output).split("\n");
615
+ const diagnostic = lines.find((line) => MESSAGE.test(line));
616
+ if (diagnostic) return MESSAGE.exec(diagnostic)?.[1] ?? diagnostic.trim();
617
+ return [...lines].reverse().find((line) => line.trim().length > 0)?.trim() ?? "no output";
618
+ }
619
+ function needsSvelteCompiler(output) {
620
+ return /svelte\/compiler/.test(output);
621
+ }
622
+
581
623
  // src/roles/format/presets/base.json
582
624
  var base_default = {
583
625
  printWidth: 80,
@@ -594,17 +636,21 @@ var base_default = {
594
636
  };
595
637
 
596
638
  // src/roles/format/preset-data.ts
597
- var FORMAT_PRESETS = ["base"];
639
+ var FORMAT_PRESETS = ["base", "svelte"];
598
640
  function hasFormatPreset(name) {
599
641
  return FORMAT_PRESETS.includes(name);
600
642
  }
601
- function formatPresetFor(_preset) {
602
- return base_default;
643
+ function formatPresetName(declared) {
644
+ return declared === "svelte" ? "svelte" : "base";
645
+ }
646
+ function formatPresetFor(preset) {
647
+ const base = base_default;
648
+ return formatPresetName(preset) === "svelte" ? { ...base, svelte: true } : base;
603
649
  }
604
650
 
605
651
  // src/roles/format/prettier-config.ts
606
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
607
- import { join as join8 } from "path";
652
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
653
+ import { join as join9 } from "path";
608
654
 
609
655
  // src/shared/jsonc.ts
610
656
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -618,17 +664,78 @@ function parseJsonc(text, source = "config") {
618
664
  return value;
619
665
  }
620
666
 
667
+ // src/roles/format/resolve-oxfmt.ts
668
+ import { readFileSync as readFileSync5 } from "fs";
669
+ import { createRequire as createRequire2 } from "module";
670
+ import { dirname as dirname4, join as join8 } from "path";
671
+ function resolveOxfmt(cwd) {
672
+ return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
673
+ }
674
+ function oxfmtRejects(key, value) {
675
+ const schema = configSchema();
676
+ const property = schema?.properties?.[key];
677
+ if (!property) return false;
678
+ const definition = resolveRef(schema, property);
679
+ if (Array.isArray(definition.enum)) return !definition.enum.includes(value);
680
+ if (definition.type === "boolean") return typeof value !== "boolean";
681
+ if (definition.type === "integer" || definition.type === "number")
682
+ return typeof value !== "number";
683
+ return false;
684
+ }
685
+ function resolveRef(schema, node) {
686
+ const ref = node.$ref ?? node.allOf?.[0]?.$ref;
687
+ const name = ref?.startsWith("#/definitions/") ? ref.slice("#/definitions/".length) : void 0;
688
+ return (name ? schema.definitions?.[name] : void 0) ?? node;
689
+ }
690
+ var cachedSchema;
691
+ function configSchema() {
692
+ if (cachedSchema !== void 0) return cachedSchema ?? void 0;
693
+ cachedSchema = readConfigSchema() ?? null;
694
+ return cachedSchema ?? void 0;
695
+ }
696
+ function readConfigSchema() {
697
+ try {
698
+ const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
699
+ const schemaPath = join8(dirname4(manifest), "configuration_schema.json");
700
+ return JSON.parse(readFileSync5(schemaPath, "utf8"));
701
+ } catch {
702
+ return void 0;
703
+ }
704
+ }
705
+
621
706
  // src/roles/format/prettier-config.ts
622
707
  var UNSUPPORTED = /* @__PURE__ */ new Set(["plugins", "parser", "filepath", "rangeStart", "rangeEnd"]);
708
+ function toOxfmtOverrides(value) {
709
+ if (!Array.isArray(value)) return { overrides: [], unresolved: [] };
710
+ const overrides = [];
711
+ const unresolved = [];
712
+ for (const entry of value) {
713
+ if (typeof entry !== "object" || entry === null) continue;
714
+ const { files, options } = entry;
715
+ const patterns = typeof files === "string" ? [files] : Array.isArray(files) ? files : [];
716
+ if (patterns.length === 0) continue;
717
+ const kept = {};
718
+ for (const [key, option] of Object.entries(options ?? {})) {
719
+ const where = `overrides[${patterns.join(", ")}].${key}`;
720
+ if (UNSUPPORTED.has(key)) unresolved.push(where);
721
+ else if (!PERMITTED_LOCAL_KEYS.includes(key)) continue;
722
+ else if (oxfmtRejects(key, option)) {
723
+ unresolved.push(`${where}: ${JSON.stringify(option)} (oxfmt does not accept that value)`);
724
+ } else kept[key] = option;
725
+ }
726
+ if (Object.keys(kept).length > 0) overrides.push({ files: patterns, options: kept });
727
+ }
728
+ return { overrides, unresolved };
729
+ }
623
730
  function readPrettierSettings(cwd) {
624
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync8(join8(cwd, name)));
731
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync8(join9(cwd, name)));
625
732
  if (!file) return { options: {}, unresolved: [] };
626
733
  if (/\.(js|cjs|mjs)$/.test(file)) {
627
734
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
628
735
  }
629
736
  let parsed;
630
737
  try {
631
- parsed = parseJsonc(readFileSync5(join8(cwd, file), "utf8"), file);
738
+ parsed = parseJsonc(readFileSync6(join9(cwd, file), "utf8"), file);
632
739
  } catch {
633
740
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
634
741
  }
@@ -636,10 +743,21 @@ function readPrettierSettings(cwd) {
636
743
  const unresolved = [];
637
744
  for (const [key, value] of Object.entries(parsed)) {
638
745
  if (UNSUPPORTED.has(key)) {
639
- unresolved.push(`${key} (oxfmt has no equivalent)`);
746
+ const named = key === "plugins" && Array.isArray(value) ? value.map((plugin) => `plugins: ${String(plugin)}`) : [key];
747
+ unresolved.push(...named.map((entry) => `${entry} (oxfmt has no equivalent)`));
748
+ continue;
749
+ }
750
+ if (key === "overrides") {
751
+ const translated = toOxfmtOverrides(value);
752
+ if (translated.overrides.length > 0) options.overrides = translated.overrides;
753
+ unresolved.push(...translated.unresolved.map((entry) => `${entry} (oxfmt has no equivalent)`));
640
754
  continue;
641
755
  }
642
756
  if (PERMITTED_LOCAL_KEYS.includes(key) || ALWAYS_LOCAL_KEYS.includes(key)) {
757
+ if (oxfmtRejects(key, value)) {
758
+ unresolved.push(`${key}: ${JSON.stringify(value)} (oxfmt does not accept that value)`);
759
+ continue;
760
+ }
643
761
  options[key] = value;
644
762
  }
645
763
  }
@@ -647,8 +765,8 @@ function readPrettierSettings(cwd) {
647
765
  }
648
766
 
649
767
  // src/roles/format/read-adoption.ts
650
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
651
- import { join as join9 } from "path";
768
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
769
+ import { join as join10 } from "path";
652
770
  var NOT_ADOPTED = (configFile, unreadable = null) => ({
653
771
  configFile,
654
772
  preset: null,
@@ -663,11 +781,11 @@ function sameValue(a, b) {
663
781
  return JSON.stringify(a) === JSON.stringify(b);
664
782
  }
665
783
  function readFormatAdoption(cwd) {
666
- const path = join9(cwd, FORMAT_CONFIG_FILE);
784
+ const path = join10(cwd, FORMAT_CONFIG_FILE);
667
785
  if (!existsSync9(path)) return NOT_ADOPTED(null);
668
786
  let parsed;
669
787
  try {
670
- parsed = parseJsonc(readFileSync6(path, "utf8"), FORMAT_CONFIG_FILE);
788
+ parsed = parseJsonc(readFileSync7(path, "utf8"), FORMAT_CONFIG_FILE);
671
789
  } catch (error) {
672
790
  const reason = error instanceof Error ? error.message : String(error);
673
791
  return NOT_ADOPTED(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
@@ -709,13 +827,8 @@ function readFormatAdoption(cwd) {
709
827
  };
710
828
  }
711
829
 
712
- // src/roles/format/resolve-oxfmt.ts
713
- function resolveOxfmt(cwd) {
714
- return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
715
- }
716
-
717
830
  // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
718
- var PRESET = "base";
831
+ var DEFAULT_PRESET = "base";
719
832
  var OXFMT_VALUE_FLAGS = ["-c", "--config", "--ignore-path", "--threads", "--stdin-filepath"];
720
833
  var OxfmtAdapter = class extends BaseAdapter {
721
834
  target = "format";
@@ -732,7 +845,8 @@ var OxfmtAdapter = class extends BaseAdapter {
732
845
  };
733
846
  }
734
847
  const own = readOwnPackage();
735
- const preset = formatPresetFor(PRESET);
848
+ const presetName = formatPresetName(context.preset);
849
+ const preset = formatPresetFor(presetName);
736
850
  const existing = readFormatAdoption(context.cwd);
737
851
  const prettier = readPrettierSettings(context.cwd);
738
852
  const local = existing.adopted ? { ...existing.local } : { ...prettier.options };
@@ -748,7 +862,7 @@ var OxfmtAdapter = class extends BaseAdapter {
748
862
  const unchanged = existing.adopted && existing.conformant;
749
863
  const version = unchanged && existing.presetVersion ? existing.presetVersion : own.version;
750
864
  const config = {
751
- [PROVENANCE_KEY]: { preset: PRESET, version, local: localKeys },
865
+ [PROVENANCE_KEY]: { preset: presetName, version, local: localKeys },
752
866
  ...preset,
753
867
  ...Object.fromEntries(
754
868
  [...localKeys, ...ALWAYS_LOCAL_KEYS].flatMap(
@@ -763,7 +877,7 @@ var OxfmtAdapter = class extends BaseAdapter {
763
877
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
764
878
  ];
765
879
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
766
- (name) => existsSync10(join10(context.cwd, name))
880
+ (name) => existsSync10(join11(context.cwd, name))
767
881
  );
768
882
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
769
883
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -772,7 +886,7 @@ var OxfmtAdapter = class extends BaseAdapter {
772
886
  }
773
887
  operations.push(...nxTargetOperations({ cwd: context.cwd, targets: formatTargets() }));
774
888
  const notes = [
775
- `materialized the ${PRESET} format preset into ${FORMAT_CONFIG_FILE} (${own.name}@${own.version}). oxfmt has no \`extends\`, so the values live here; \`sentinel --status --format\` reports modules left behind by a preset change`
889
+ `materialized the ${presetName} format preset into ${FORMAT_CONFIG_FILE} (${own.name}@${own.version}). oxfmt has no \`extends\`, so the values live here; \`sentinel --status --format\` reports modules left behind by a preset change`
776
890
  ];
777
891
  if (localKeys.length > 0) {
778
892
  notes.push(
@@ -784,9 +898,12 @@ var OxfmtAdapter = class extends BaseAdapter {
784
898
  `carried ${inherited.length} ignore pattern(s) from the workspace .prettierignore (${inherited.join(", ")}). oxfmt reads that file from the CURRENT directory and sentinel runs it in this module, so without them adoption would start formatting files the repo has always excluded`
785
899
  );
786
900
  }
787
- if (prettier.unresolved.length > 0) {
901
+ const lost = prettier.unresolved.filter(
902
+ (entry) => !(presetName === "svelte" && /svelte/i.test(entry))
903
+ );
904
+ if (lost.length > 0) {
788
905
  notes.push(
789
- `COULD NOT carry ${prettier.unresolved.join("; ")}. Check it and add what you need to ${FORMAT_CONFIG_FILE}, declaring the key in \`${PROVENANCE_KEY}.local\` so a re-init keeps it`
906
+ `COULD NOT carry ${lost.join("; ")}. Check it and add what you need to ${FORMAT_CONFIG_FILE}, declaring the key in \`${PROVENANCE_KEY}.local\` so a re-init keeps it`
790
907
  );
791
908
  }
792
909
  if (prettierConfigs.length > 0) {
@@ -819,7 +936,45 @@ var OxfmtAdapter = class extends BaseAdapter {
819
936
  async afterInit(ctx) {
820
937
  const oxfmt = resolveOxfmt(ctx.cwd);
821
938
  if (!oxfmt) return;
822
- spawnSync(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
939
+ const pass = spawnSync(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
940
+ if (pass.status === 0) return;
941
+ const output = `${pass.stdout ?? ""}${pass.stderr ?? ""}`;
942
+ const unparseable = unparseableFiles(output);
943
+ const warn = palette(process.stderr).warn;
944
+ if (unparseable.length > 0) {
945
+ process.stderr.write(
946
+ warn(
947
+ ` ${unparseable.length} file(s) oxfmt cannot parse, so they were NOT formatted and \`pnpm run ${FORMAT_SCRIPT_NAME}\` fails until they are fixed:
948
+ `
949
+ )
950
+ );
951
+ for (const { file, line, message } of unparseable) {
952
+ process.stderr.write(warn(` ${file}:${line} ${message}
953
+ `));
954
+ }
955
+ process.stderr.write(
956
+ palette(process.stderr).dim(
957
+ ` These are syntax errors Prettier recovered from and oxfmt refuses. They are the module's, not the migration's: fix them (usually one line), or exclude them in \`ignorePatterns\`.
958
+ `
959
+ )
960
+ );
961
+ return;
962
+ }
963
+ if (needsSvelteCompiler(output)) {
964
+ process.stderr.write(
965
+ warn(
966
+ ` .svelte files are not formatted yet: oxfmt loads the Svelte compiler from this module, and it is not installed here. Run \`pnpm install\`, then \`pnpm run ${FORMAT_SCRIPT_NAME}:fix\`.
967
+ `
968
+ )
969
+ );
970
+ return;
971
+ }
972
+ process.stderr.write(
973
+ warn(
974
+ ` the format pass did not finish, so this module is not formatted yet: ${oxfmtSaid(output)}
975
+ `
976
+ )
977
+ );
823
978
  }
824
979
  /** Check formatting; `--fix` writes. */
825
980
  async run(ctx) {
@@ -870,7 +1025,7 @@ var OxfmtAdapter = class extends BaseAdapter {
870
1025
  */
871
1026
  async inspect(ctx) {
872
1027
  const adoption = readFormatAdoption(ctx.cwd);
873
- const preset = formatPresetFor(adoption.preset ?? PRESET);
1028
+ const preset = formatPresetFor(adoption.preset ?? DEFAULT_PRESET);
874
1029
  const options = { ...preset, ...adoption.local };
875
1030
  const describe = (value) => typeof value === "string" ? value : JSON.stringify(value);
876
1031
  return {
@@ -881,7 +1036,7 @@ var OxfmtAdapter = class extends BaseAdapter {
881
1036
  options,
882
1037
  overrides: Object.entries(adoption.local).filter(([key]) => !ALWAYS_LOCAL_KEYS.includes(key)).map(([key, value]) => ({
883
1038
  rule: key,
884
- reason: key in preset ? `${describe(value)}, where the ${adoption.preset ?? PRESET} preset says ${describe(preset[key])}` : `${describe(value)}, which the preset does not set`
1039
+ reason: key in preset ? `${describe(value)}, where the ${adoption.preset ?? DEFAULT_PRESET} preset says ${describe(preset[key])}` : `${describe(value)}, which the preset does not set`
885
1040
  })),
886
1041
  // Which files this module does not format at all. A long list here is worth a look:
887
1042
  // it is the quiet way a module stops being covered.
@@ -948,7 +1103,7 @@ var OxfmtAdapter = class extends BaseAdapter {
948
1103
  modulePrettierDependencies(cwd) {
949
1104
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
950
1105
  try {
951
- const manifest = JSON.parse(readFileSync7(join10(cwd, "package.json"), "utf8"));
1106
+ const manifest = JSON.parse(readFileSync8(join11(cwd, "package.json"), "utf8"));
952
1107
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
953
1108
  } catch {
954
1109
  return [];
@@ -981,7 +1136,7 @@ var OxfmtAdapter = class extends BaseAdapter {
981
1136
  // script too and rewrote it into a second format run.
982
1137
  ownCommand !== void 0 && /\bsentinel\b/.test(command) && /--format\b/.test(command);
983
1138
  if (!needsRewrite) continue;
984
- rewritten[name] = compose(command, /--write\b/.test(command) || /:(fix|write)$/.test(name));
1139
+ rewritten[name] = compose(command, writesWhenRewritten(name, command));
985
1140
  }
986
1141
  return this.joinFormatIntoLint(scripts, rewritten, ownCommand);
987
1142
  }
@@ -1028,8 +1183,8 @@ function registerFormat() {
1028
1183
 
1029
1184
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1030
1185
  import { spawnSync as spawnSync2 } from "child_process";
1031
- import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
1032
- import { join as join17 } from "path";
1186
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
1187
+ import { join as join18 } from "path";
1033
1188
 
1034
1189
  // src/core/config/deferred-rules.ts
1035
1190
  function deferredRuleNames(rules) {
@@ -2864,8 +3019,8 @@ function parseOxlintDiagnostics(stdout) {
2864
3019
  }
2865
3020
 
2866
3021
  // src/core/config/read-adoption.ts
2867
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
2868
- import { join as join12 } from "path";
3022
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
3023
+ import { join as join13 } from "path";
2869
3024
 
2870
3025
  // src/core/config/owned-keys.ts
2871
3026
  function presetOwnedKeys(config, permitted) {
@@ -2874,12 +3029,12 @@ function presetOwnedKeys(config, permitted) {
2874
3029
  }
2875
3030
 
2876
3031
  // src/core/config/resolve-config-target.ts
2877
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
2878
- import { join as join11 } from "path";
3032
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3033
+ import { join as join12 } from "path";
2879
3034
  function readExtends(absolutePath) {
2880
3035
  let parsed;
2881
3036
  try {
2882
- parsed = parseJsonc(readFileSync8(absolutePath, "utf8"), absolutePath);
3037
+ parsed = parseJsonc(readFileSync9(absolutePath, "utf8"), absolutePath);
2883
3038
  } catch {
2884
3039
  return [];
2885
3040
  }
@@ -2893,7 +3048,7 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
2893
3048
  let existing;
2894
3049
  let existingExtendsSomething = false;
2895
3050
  for (const candidate of candidates) {
2896
- const absolutePath = join11(moduleDir, candidate);
3051
+ const absolutePath = join12(moduleDir, candidate);
2897
3052
  if (!existsSync11(absolutePath)) continue;
2898
3053
  const chain = readExtends(absolutePath);
2899
3054
  if (existing === void 0) {
@@ -2927,12 +3082,12 @@ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
2927
3082
  });
2928
3083
  function readAdoption(cwd, options) {
2929
3084
  const target = resolveConfigTarget(cwd, options);
2930
- if (target.reason === "none" || !existsSync12(join12(cwd, target.path))) {
3085
+ if (target.reason === "none" || !existsSync12(join13(cwd, target.path))) {
2931
3086
  return NOT_ADOPTED2(target.reason === "none" ? null : target.path);
2932
3087
  }
2933
3088
  let parsed;
2934
3089
  try {
2935
- parsed = parseJsonc(readFileSync9(join12(cwd, target.path), "utf8"), target.path);
3090
+ parsed = parseJsonc(readFileSync10(join13(cwd, target.path), "utf8"), target.path);
2936
3091
  } catch (error) {
2937
3092
  const reason = error instanceof Error ? error.message : String(error);
2938
3093
  return NOT_ADOPTED2(target.path, `${target.path} could not be parsed (${reason})`);
@@ -2967,14 +3122,14 @@ function readLintAdoption(cwd) {
2967
3122
 
2968
3123
  // src/roles/lint/resolve-oxlint.ts
2969
3124
  import { existsSync as existsSync13 } from "fs";
2970
- import { createRequire as createRequire2 } from "module";
2971
- import { delimiter as delimiter2, dirname as dirname4, join as join13 } from "path";
3125
+ import { createRequire as createRequire3 } from "module";
3126
+ import { delimiter as delimiter2, dirname as dirname5, join as join14 } from "path";
2972
3127
  import { fileURLToPath as fileURLToPath2 } from "url";
2973
3128
  var PACKAGE_OF = {
2974
3129
  oxlint: "oxlint",
2975
3130
  tsgolint: "oxlint-tsgolint"
2976
3131
  };
2977
- var require3 = createRequire2(import.meta.url);
3132
+ var require3 = createRequire3(import.meta.url);
2978
3133
  function resolveOxlint(cwd, name = "oxlint") {
2979
3134
  return resolveBin(cwd, name) ?? binFromOwnInstall(PACKAGE_OF[name], name);
2980
3135
  }
@@ -2984,7 +3139,7 @@ function canRunTypeAware(cwd) {
2984
3139
  function oxlintPath(cwd, env) {
2985
3140
  const shim = tsgolintShim(cwd);
2986
3141
  if (!shim) return env.PATH;
2987
- return [dirname4(shim), env.PATH].filter(Boolean).join(delimiter2);
3142
+ return [dirname5(shim), env.PATH].filter(Boolean).join(delimiter2);
2988
3143
  }
2989
3144
  function tsgolintShim(cwd) {
2990
3145
  const fromModule = resolveBin(cwd, "tsgolint");
@@ -2992,17 +3147,17 @@ function tsgolintShim(cwd) {
2992
3147
  const candidates = [];
2993
3148
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
2994
3149
  try {
2995
- const packageDir = dirname4(require3.resolve(`${owner}/package.json`));
2996
- candidates.push(join13(packageDir, "node_modules", ".bin", "tsgolint"));
2997
- candidates.push(join13(packageDir, "..", ".bin", "tsgolint"));
3150
+ const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
3151
+ candidates.push(join14(packageDir, "node_modules", ".bin", "tsgolint"));
3152
+ candidates.push(join14(packageDir, "..", ".bin", "tsgolint"));
2998
3153
  } catch {
2999
3154
  }
3000
3155
  }
3001
3156
  try {
3002
- const ownRoot = dirname4(require3.resolve("@hublo/sentinel/package.json"));
3003
- candidates.push(join13(ownRoot, "node_modules", ".bin", "tsgolint"));
3157
+ const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
3158
+ candidates.push(join14(ownRoot, "node_modules", ".bin", "tsgolint"));
3004
3159
  } catch {
3005
- candidates.push(resolveBin(dirname4(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
3160
+ candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
3006
3161
  }
3007
3162
  return candidates.find((candidate) => candidate !== "" && existsSync13(candidate));
3008
3163
  }
@@ -3011,12 +3166,12 @@ function oxlintSearchPath(cwd) {
3011
3166
  }
3012
3167
 
3013
3168
  // src/roles/lint/adapters/oxlint/plan.ts
3014
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
3015
- import { join as join16 } from "path";
3169
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3170
+ import { join as join17 } from "path";
3016
3171
 
3017
3172
  // src/roles/lint/eslint-ignores.ts
3018
- import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
3019
- import { join as join14 } from "path";
3173
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
3174
+ import { join as join15 } from "path";
3020
3175
  var IGNORE_BLOCKS = [/\bignores\s*:\s*\[([^\]]*)\]/g, /\bglobalIgnores\s*\(\s*\[([^\]]*)\]/g];
3021
3176
  var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
3022
3177
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
@@ -3029,11 +3184,11 @@ function readRootEslintIgnores(root) {
3029
3184
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
3030
3185
  }
3031
3186
  function readEslintIgnores(cwd) {
3032
- const config = ESLINT_CONFIG_FILES.map((name) => join14(cwd, name)).find((path) => existsSync14(path));
3187
+ const config = ESLINT_CONFIG_FILES.map((name) => join15(cwd, name)).find((path) => existsSync14(path));
3033
3188
  if (!config) return { patterns: [], unresolved: [] };
3034
3189
  let source;
3035
3190
  try {
3036
- source = readFileSync10(config, "utf8");
3191
+ source = readFileSync11(config, "utf8");
3037
3192
  } catch {
3038
3193
  return { patterns: [], unresolved: [] };
3039
3194
  }
@@ -3087,8 +3242,8 @@ function lintPresetFor(preset) {
3087
3242
  }
3088
3243
 
3089
3244
  // src/roles/lint/rename-suppressions.ts
3090
- import { readdirSync as readdirSync2, readFileSync as readFileSync11, statSync } from "fs";
3091
- import { join as join15, relative as relative2 } from "path";
3245
+ import { readdirSync as readdirSync2, readFileSync as readFileSync12, statSync } from "fs";
3246
+ import { join as join16, relative as relative2 } from "path";
3092
3247
  var SOURCE_EXTENSIONS = [
3093
3248
  ".ts",
3094
3249
  ".tsx",
@@ -3135,7 +3290,7 @@ function* sourceFiles(dir) {
3135
3290
  return;
3136
3291
  }
3137
3292
  for (const entry of entries) {
3138
- const full = join15(dir, entry);
3293
+ const full = join16(dir, entry);
3139
3294
  let isDirectory;
3140
3295
  try {
3141
3296
  isDirectory = statSync(full).isDirectory();
@@ -3155,7 +3310,7 @@ function findSuppressionRenames(cwd, renames) {
3155
3310
  for (const file of sourceFiles(cwd)) {
3156
3311
  let content;
3157
3312
  try {
3158
- content = readFileSync11(file, "utf8");
3313
+ content = readFileSync12(file, "utf8");
3159
3314
  } catch {
3160
3315
  continue;
3161
3316
  }
@@ -3228,7 +3383,7 @@ function plan(context) {
3228
3383
  if (!hasLintPreset(context.preset)) {
3229
3384
  return {
3230
3385
  operations: [],
3231
- blocked: `sentinel does not ship a lint preset named "${context.preset}" yet (shipped: ${LINT_PRESETS.join(", ")}). Nothing was written; this module cannot adopt the lint preset until that preset ships.`
3386
+ skipped: `sentinel does not ship a lint preset named "${context.preset}" yet (shipped: ${LINT_PRESETS.join(", ")}). Nothing was written; the module keeps its current linter and the other roles adopt normally.`
3232
3387
  };
3233
3388
  }
3234
3389
  const current = presetOfVariant(presetNameFromPath(readLintAdoption(context.cwd).preset));
@@ -3273,7 +3428,7 @@ function plan(context) {
3273
3428
  keys: removableDeps
3274
3429
  });
3275
3430
  }
3276
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync15(join16(context.cwd, name)));
3431
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync15(join17(context.cwd, name)));
3277
3432
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
3278
3433
  operations.push(
3279
3434
  ...nxTargetOperations({
@@ -3368,7 +3523,7 @@ function lintScripts(cwd) {
3368
3523
  function committedIgnorePatterns(cwd) {
3369
3524
  try {
3370
3525
  const parsed = parseJsonc(
3371
- readFileSync12(join16(cwd, LINT_CONFIG_FILE), "utf8"),
3526
+ readFileSync13(join17(cwd, LINT_CONFIG_FILE), "utf8"),
3372
3527
  LINT_CONFIG_FILE
3373
3528
  );
3374
3529
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -3380,7 +3535,7 @@ function moduleEslintDependencies(cwd) {
3380
3535
  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/");
3381
3536
  let manifest;
3382
3537
  try {
3383
- manifest = JSON.parse(readFileSync12(join16(cwd, "package.json"), "utf8"));
3538
+ manifest = JSON.parse(readFileSync13(join17(cwd, "package.json"), "utf8"));
3384
3539
  } catch {
3385
3540
  return [];
3386
3541
  }
@@ -3479,7 +3634,7 @@ var OxlintAdapter = class extends BaseAdapter {
3479
3634
  this.fixPass(ctx, oxlint, env);
3480
3635
  }
3481
3636
  async run(ctx) {
3482
- if (!existsSync16(join17(ctx.cwd, LINT_CONFIG_FILE))) {
3637
+ if (!existsSync16(join18(ctx.cwd, LINT_CONFIG_FILE))) {
3483
3638
  process.stderr.write(
3484
3639
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
3485
3640
  `
@@ -3572,9 +3727,9 @@ var OxlintAdapter = class extends BaseAdapter {
3572
3727
  if (target) return 0;
3573
3728
  let total = 0;
3574
3729
  try {
3575
- const stub = JSON.parse(readFileSync13(join17(cwd, LINT_CONFIG_FILE), "utf8"));
3730
+ const stub = JSON.parse(readFileSync14(join18(cwd, LINT_CONFIG_FILE), "utf8"));
3576
3731
  for (const entry of stub.extends ?? []) {
3577
- const preset = JSON.parse(readFileSync13(join17(cwd, entry), "utf8"));
3732
+ const preset = JSON.parse(readFileSync14(join18(cwd, entry), "utf8"));
3578
3733
  total += Object.keys(preset.rules ?? {}).length;
3579
3734
  }
3580
3735
  } catch {
@@ -3642,12 +3797,12 @@ var OxlintAdapter = class extends BaseAdapter {
3642
3797
  unresolvedPreset(cwd) {
3643
3798
  let parsed;
3644
3799
  try {
3645
- parsed = JSON.parse(readFileSync13(join17(cwd, LINT_CONFIG_FILE), "utf8"));
3800
+ parsed = JSON.parse(readFileSync14(join18(cwd, LINT_CONFIG_FILE), "utf8"));
3646
3801
  } catch {
3647
3802
  return void 0;
3648
3803
  }
3649
3804
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
3650
- return targets.find((target) => !existsSync16(join17(cwd, target)));
3805
+ return targets.find((target) => !existsSync16(join18(cwd, target)));
3651
3806
  }
3652
3807
  /** Announce what is not enforced, so reduced coverage is never silent. */
3653
3808
  announceDisabled(preset) {
@@ -3679,8 +3834,8 @@ function registerLint() {
3679
3834
 
3680
3835
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
3681
3836
  import { spawnSync as spawnSync3 } from "child_process";
3682
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
3683
- import { createRequire as createRequire3 } from "module";
3837
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
3838
+ import { createRequire as createRequire4 } from "module";
3684
3839
  import { join as join20 } from "path";
3685
3840
 
3686
3841
  // src/roles/typescript/presets/base.json
@@ -3844,86 +3999,6 @@ function readTsconfigAdoption(cwd) {
3844
3999
  import { readFileSync as readFileSync15 } from "fs";
3845
4000
  import { join as join19 } from "path";
3846
4001
 
3847
- // src/core/config/preset-evidence.ts
3848
- import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync14 } from "fs";
3849
- import { join as join18 } from "path";
3850
- var PATH_SIGNALS = [
3851
- {
3852
- preset: "nest",
3853
- pattern: /(^|\/)apps\/nest\//,
3854
- evidence: "it lives under apps/nest"
3855
- }
3856
- ];
3857
- var DEPENDENCY_SIGNALS = [
3858
- { preset: "react", pattern: /^(react|react-dom|@types\/react)$|^eslint-plugin-react/ },
3859
- { preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
3860
- { preset: "nest", pattern: /^@nestjs\// }
3861
- ];
3862
- function dependencyNames(cwd) {
3863
- const path = join18(cwd, "package.json");
3864
- if (!existsSync17(path)) return [];
3865
- try {
3866
- const manifest = parseJsonc(
3867
- readFileSync14(path, "utf8"),
3868
- path
3869
- );
3870
- return [
3871
- ...Object.keys(manifest.dependencies ?? {}),
3872
- ...Object.keys(manifest.devDependencies ?? {})
3873
- ];
3874
- } catch {
3875
- return [];
3876
- }
3877
- }
3878
- function declaresJsx(cwd) {
3879
- let entries;
3880
- try {
3881
- entries = readdirSync3(cwd).filter(
3882
- (name) => name.startsWith("tsconfig") && name.endsWith(".json")
3883
- );
3884
- } catch {
3885
- return false;
3886
- }
3887
- for (const name of entries) {
3888
- try {
3889
- const config = parseJsonc(
3890
- readFileSync14(join18(cwd, name), "utf8"),
3891
- name
3892
- );
3893
- if (config.compilerOptions?.jsx !== void 0) return true;
3894
- } catch {
3895
- }
3896
- }
3897
- return false;
3898
- }
3899
- function presetSignals(cwd) {
3900
- const signals = [];
3901
- const dependencies = dependencyNames(cwd);
3902
- for (const { preset, pattern } of DEPENDENCY_SIGNALS) {
3903
- const hit = dependencies.find((name) => pattern.test(name));
3904
- if (hit) signals.push({ preset, evidence: `it depends on "${hit}"` });
3905
- }
3906
- if (declaresJsx(cwd) && !signals.some((signal) => signal.preset === "react")) {
3907
- signals.push({ preset: "react", evidence: 'its tsconfig sets "jsx"' });
3908
- }
3909
- const location = cwd.replaceAll("\\", "/");
3910
- for (const { preset, pattern, evidence } of PATH_SIGNALS) {
3911
- if (pattern.test(location) && !signals.some((signal) => signal.preset === preset)) {
3912
- signals.push({ preset, evidence });
3913
- }
3914
- }
3915
- return signals;
3916
- }
3917
- function presetContradiction(declared, cwd) {
3918
- const signals = presetSignals(cwd);
3919
- if (signals.length === 0) return void 0;
3920
- if (signals.some((signal) => signal.preset === declared)) return void 0;
3921
- const [first] = signals;
3922
- if (!first) return void 0;
3923
- const evidence = signals.map((signal) => signal.evidence).join(", and ");
3924
- return `--preset ${declared} does not match this module: ${evidence}, which makes it "${first.preset}". Nothing was written. Re-run with --preset ${first.preset}, or adopt from the module you actually meant.`;
3925
- }
3926
-
3927
4002
  // src/roles/typescript/typecheck-script.ts
3928
4003
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
3929
4004
  var TYPECHECK_SCRIPT_NAME = "typecheck";
@@ -3964,7 +4039,7 @@ function planAdoption(context) {
3964
4039
  if (!hasShippedPreset(context.preset)) {
3965
4040
  return {
3966
4041
  operations: [],
3967
- blocked: `sentinel does not ship a TypeScript preset named "${context.preset}" yet (shipped: ${SHIPPED_PRESETS.join(", ")}). Nothing was written; this module cannot adopt the TypeScript preset until that preset ships.`
4042
+ skipped: `sentinel does not ship a TypeScript preset named "${context.preset}" yet (shipped: ${SHIPPED_PRESETS.join(", ")}). Nothing was written, and the module keeps its own tsconfig; the other roles adopt normally.`
3968
4043
  };
3969
4044
  }
3970
4045
  const current = declaredPreset(context.cwd);
@@ -3974,10 +4049,6 @@ function planAdoption(context) {
3974
4049
  blocked: `this module is already adopted as "${current}", and --preset says "${context.preset}". Nothing was written. If the change is deliberate, remove the sentinel preset from the tsconfig \`extends\` chain and re-run; otherwise re-run with --preset ${current}.`
3975
4050
  };
3976
4051
  }
3977
- const contradiction = presetContradiction(context.preset, context.cwd);
3978
- if (contradiction !== void 0) {
3979
- return { operations: [], blocked: contradiction };
3980
- }
3981
4052
  if (!hasSourceFiles(context.cwd, TYPECHECKABLE_EXTENSIONS)) {
3982
4053
  return {
3983
4054
  operations: [],
@@ -4156,7 +4227,7 @@ var TscAdapter = class extends BaseAdapter {
4156
4227
  );
4157
4228
  if (preset === void 0) return void 0;
4158
4229
  try {
4159
- createRequire3(join20(cwd, "noop.js")).resolve(preset);
4230
+ createRequire4(join20(cwd, "noop.js")).resolve(preset);
4160
4231
  return void 0;
4161
4232
  } catch {
4162
4233
  return preset;
@@ -4261,7 +4332,7 @@ var TscAdapter = class extends BaseAdapter {
4261
4332
  * check.
4262
4333
  */
4263
4334
  typecheckTarget(cwd) {
4264
- if (existsSync18(join20(cwd, "tsconfig.json"))) return "tsconfig.json";
4335
+ if (existsSync17(join20(cwd, "tsconfig.json"))) return "tsconfig.json";
4265
4336
  const target = resolveTsconfigTarget(cwd);
4266
4337
  return target.reason === "none" ? null : target.path;
4267
4338
  }
@@ -4451,7 +4522,7 @@ function replaceLines(current, replacements) {
4451
4522
  }
4452
4523
 
4453
4524
  // src/core/apply-plan.ts
4454
- import { existsSync as existsSync19, readFileSync as readFileSync17, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
4525
+ import { existsSync as existsSync18, readFileSync as readFileSync17, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
4455
4526
  import { resolve as resolve3, sep } from "path";
4456
4527
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
4457
4528
 
@@ -4470,7 +4541,7 @@ function resolveWithinRoot(cwd, relativePath) {
4470
4541
  return absolutePath;
4471
4542
  }
4472
4543
  function readIfExists(absolutePath) {
4473
- return existsSync19(absolutePath) ? readFileSync17(absolutePath, "utf8") : void 0;
4544
+ return existsSync18(absolutePath) ? readFileSync17(absolutePath, "utf8") : void 0;
4474
4545
  }
4475
4546
  function* leaves(value, prefix = []) {
4476
4547
  for (const [key, keyValue] of Object.entries(value)) {
@@ -4587,6 +4658,86 @@ function applyPlan(cwd, plan2) {
4587
4658
  return changed.map((file) => ({ path: file.path, deleted: Boolean(file.deleted) }));
4588
4659
  }
4589
4660
 
4661
+ // src/core/config/preset-evidence.ts
4662
+ import { existsSync as existsSync19, readdirSync as readdirSync3, readFileSync as readFileSync18 } from "fs";
4663
+ import { join as join21 } from "path";
4664
+ var PATH_SIGNALS = [
4665
+ {
4666
+ preset: "nest",
4667
+ pattern: /(^|\/)apps\/nest\//,
4668
+ evidence: "it lives under apps/nest"
4669
+ }
4670
+ ];
4671
+ var DEPENDENCY_SIGNALS = [
4672
+ { preset: "react", pattern: /^(react|react-dom|@types\/react)$|^eslint-plugin-react/ },
4673
+ { preset: "svelte", pattern: /^svelte$|^@sveltejs\// },
4674
+ { preset: "nest", pattern: /^@nestjs\// }
4675
+ ];
4676
+ function dependencyNames(cwd) {
4677
+ const path = join21(cwd, "package.json");
4678
+ if (!existsSync19(path)) return [];
4679
+ try {
4680
+ const manifest = parseJsonc(
4681
+ readFileSync18(path, "utf8"),
4682
+ path
4683
+ );
4684
+ return [
4685
+ ...Object.keys(manifest.dependencies ?? {}),
4686
+ ...Object.keys(manifest.devDependencies ?? {})
4687
+ ];
4688
+ } catch {
4689
+ return [];
4690
+ }
4691
+ }
4692
+ function declaresJsx(cwd) {
4693
+ let entries;
4694
+ try {
4695
+ entries = readdirSync3(cwd).filter(
4696
+ (name) => name.startsWith("tsconfig") && name.endsWith(".json")
4697
+ );
4698
+ } catch {
4699
+ return false;
4700
+ }
4701
+ for (const name of entries) {
4702
+ try {
4703
+ const config = parseJsonc(
4704
+ readFileSync18(join21(cwd, name), "utf8"),
4705
+ name
4706
+ );
4707
+ if (config.compilerOptions?.jsx !== void 0) return true;
4708
+ } catch {
4709
+ }
4710
+ }
4711
+ return false;
4712
+ }
4713
+ function presetSignals(cwd) {
4714
+ const signals = [];
4715
+ const dependencies = dependencyNames(cwd);
4716
+ for (const { preset, pattern } of DEPENDENCY_SIGNALS) {
4717
+ const hit = dependencies.find((name) => pattern.test(name));
4718
+ if (hit) signals.push({ preset, evidence: `it depends on "${hit}"` });
4719
+ }
4720
+ if (declaresJsx(cwd) && !signals.some((signal) => signal.preset === "react")) {
4721
+ signals.push({ preset: "react", evidence: 'its tsconfig sets "jsx"' });
4722
+ }
4723
+ const location = cwd.replaceAll("\\", "/");
4724
+ for (const { preset, pattern, evidence } of PATH_SIGNALS) {
4725
+ if (pattern.test(location) && !signals.some((signal) => signal.preset === preset)) {
4726
+ signals.push({ preset, evidence });
4727
+ }
4728
+ }
4729
+ return signals;
4730
+ }
4731
+ function presetContradiction(declared, cwd) {
4732
+ const signals = presetSignals(cwd);
4733
+ if (signals.length === 0) return void 0;
4734
+ if (signals.some((signal) => signal.preset === declared)) return void 0;
4735
+ const [first] = signals;
4736
+ if (!first) return void 0;
4737
+ const evidence = signals.map((signal) => signal.evidence).join(", and ");
4738
+ return `--preset ${declared} does not match this module: ${evidence}, which makes it "${first.preset}". Nothing was written. Re-run with --preset ${first.preset}, or adopt from the module you actually meant.`;
4739
+ }
4740
+
4590
4741
  // src/core/dispatch.ts
4591
4742
  function resolveFlavour(opts) {
4592
4743
  if (opts.preset) return opts.preset;
@@ -4646,6 +4797,12 @@ async function dispatch(opts) {
4646
4797
  const detected = resolveFlavour(opts);
4647
4798
  const adapter = resolve(opts.target, detected, opts.runner);
4648
4799
  const preset = opts.preset ?? adapter.declaredPreset?.(opts.cwd) ?? detected;
4800
+ const contradiction = presetContradiction(preset, opts.cwd);
4801
+ if (contradiction !== void 0) {
4802
+ process.stderr.write(`sentinel (${opts.target}): ${contradiction}
4803
+ `);
4804
+ return 1;
4805
+ }
4649
4806
  const context = { cwd: opts.cwd, preset };
4650
4807
  const plan2 = await adapter.plan(context);
4651
4808
  if (plan2.blocked) {
@@ -4694,8 +4851,8 @@ export {
4694
4851
  WORKSPACE_ROOT_MARKER,
4695
4852
  findWorkspaceRoot,
4696
4853
  ensureWorkspacePrep,
4697
- resolveBin,
4698
4854
  palette,
4855
+ resolveBin,
4699
4856
  VERBS,
4700
4857
  TARGETS,
4701
4858
  PRESET_NAMES,
@@ -4704,4 +4861,4 @@ export {
4704
4861
  detectFramework,
4705
4862
  dispatch
4706
4863
  };
4707
- //# sourceMappingURL=chunk-NAUACDXT.js.map
4864
+ //# sourceMappingURL=chunk-U2NAKQXR.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-NAUACDXT.js";
10
+ } from "./chunk-U2NAKQXR.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.1.0-alpha.5",
3
+ "version": "1.1.0-alpha.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",