@hublo/sentinel 1.1.7 → 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,5 +1,21 @@
1
+ import {
2
+ REACT_APP_DEFAULTS,
3
+ REQUIREMENTS,
4
+ isPlainObject
5
+ } from "./chunk-XVDOQ3G3.js";
6
+
1
7
  // src/core/registry.ts
2
8
  var adapters = [];
9
+ var PresetUnsupportedError = class extends Error {
10
+ constructor(target, preset) {
11
+ super(`No adapter for target "${target}" handles preset "${preset}".`);
12
+ this.target = target;
13
+ this.preset = preset;
14
+ this.name = "PresetUnsupportedError";
15
+ }
16
+ target;
17
+ preset;
18
+ };
3
19
  var defaultRunner = {
4
20
  // Filled in by tool branches, e.g. lint: 'eslint', typescript: 'tsc'.
5
21
  };
@@ -12,6 +28,14 @@ function setDefaultRunner(target, runner) {
12
28
  function all() {
13
29
  return adapters;
14
30
  }
31
+ function declaredPresetFor(target, cwd) {
32
+ for (const adapter of adapters) {
33
+ if (adapter.target !== target) continue;
34
+ const declared = adapter.declaredPreset?.(cwd);
35
+ if (declared !== void 0) return declared;
36
+ }
37
+ return void 0;
38
+ }
15
39
  function availableTargets() {
16
40
  return [...new Set(adapters.map((a) => a.target))];
17
41
  }
@@ -24,7 +48,7 @@ function resolve(target, preset, runner) {
24
48
  }
25
49
  const candidates = preset ? forTarget.filter((a) => a.appliesTo(preset)) : forTarget;
26
50
  if (candidates.length === 0) {
27
- throw new Error(`No adapter for target "${target}" handles preset "${preset}".`);
51
+ throw new PresetUnsupportedError(target, preset);
28
52
  }
29
53
  const wanted = runner ?? defaultRunner[target];
30
54
  const available = candidates.map((a) => a.runner).join(", ");
@@ -117,11 +141,16 @@ var TARGETS = [
117
141
  "format",
118
142
  "typescript",
119
143
  "build",
144
+ "dev",
120
145
  "test",
121
146
  "static-analysis",
122
147
  "runtime-analysis",
123
148
  "arch"
124
149
  ];
150
+ var LONG_RUNNING_TARGETS = ["dev"];
151
+ var SWEEPABLE_TARGETS = TARGETS.filter(
152
+ (target) => !LONG_RUNNING_TARGETS.includes(target)
153
+ );
125
154
  var PRESET_NAMES = ["react", "nest", "svelte", "node", "tools"];
126
155
 
127
156
  // src/shared/color.ts
@@ -138,14 +167,67 @@ function palette(stream) {
138
167
  };
139
168
  }
140
169
 
141
- // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
170
+ // src/roles/build/adapters/vite/vite-role.adapter.ts
142
171
  import { spawnSync } from "child_process";
143
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
144
- import { join as join12 } from "path";
172
+
173
+ // src/core/config/tool-args.ts
174
+ import { existsSync as existsSync2 } from "fs";
175
+ import { isAbsolute, resolve as resolve2 } from "path";
176
+ function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
177
+ const takesValue = new Set(valueFlags);
178
+ const options = [];
179
+ const paths = [];
180
+ let previousTakesValue = false;
181
+ for (const arg of toolArgs) {
182
+ const looksLikeOption = arg.startsWith("-");
183
+ const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
184
+ if (!previousTakesValue && !looksLikeOption && existsSync2(target)) paths.push(arg);
185
+ else options.push(arg);
186
+ previousTakesValue = !arg.includes("=") && takesValue.has(arg);
187
+ }
188
+ return { options, paths };
189
+ }
190
+
191
+ // src/shared/resolve-bin.ts
192
+ import { existsSync as existsSync3 } from "fs";
193
+ import { createRequire } from "module";
194
+ import { delimiter, dirname as dirname2, join as join2 } from "path";
195
+ var require2 = createRequire(import.meta.url);
196
+ function resolveBin(fromDir, name) {
197
+ let dir = fromDir;
198
+ for (; ; ) {
199
+ const candidate = join2(dir, "node_modules", ".bin", name);
200
+ if (existsSync3(candidate)) return candidate;
201
+ const parent = dirname2(dir);
202
+ if (parent === dir) return void 0;
203
+ dir = parent;
204
+ }
205
+ }
206
+ function binFromOwnInstall(packageName, binName) {
207
+ try {
208
+ const manifest = require2.resolve(`${packageName}/package.json`);
209
+ const bin = require2(manifest).bin;
210
+ const relative4 = typeof bin === "string" ? bin : bin?.[binName];
211
+ if (!relative4) return void 0;
212
+ const executable = join2(dirname2(manifest), relative4);
213
+ return existsSync3(executable) ? executable : void 0;
214
+ } catch {
215
+ return void 0;
216
+ }
217
+ }
218
+ function binSearchPath(cwd) {
219
+ return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
220
+ " then "
221
+ );
222
+ }
223
+
224
+ // src/roles/build/plan.ts
225
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
226
+ import { join as join7 } from "path";
145
227
 
146
228
  // src/core/config/existing-command.ts
147
229
  import { readFileSync as readFileSync3 } from "fs";
148
- import { join as join3 } from "path";
230
+ import { join as join4 } from "path";
149
231
 
150
232
  // src/shared/jsonc.ts
151
233
  import { parse, printParseErrorCode } from "jsonc-parser";
@@ -160,11 +242,11 @@ function parseJsonc(text, source = "config") {
160
242
  }
161
243
 
162
244
  // src/core/config/manifest.ts
163
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
164
- import { basename, join as join2 } from "path";
245
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
246
+ import { basename, join as join3 } from "path";
165
247
  function moduleScripts(cwd) {
166
248
  try {
167
- const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
249
+ const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
168
250
  return pkg.scripts ?? {};
169
251
  } catch {
170
252
  return {};
@@ -172,7 +254,7 @@ function moduleScripts(cwd) {
172
254
  }
173
255
  function moduleName(cwd) {
174
256
  try {
175
- const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
257
+ const pkg = JSON.parse(readFileSync2(join3(cwd, "package.json"), "utf8"));
176
258
  return pkg.name;
177
259
  } catch {
178
260
  return void 0;
@@ -192,7 +274,7 @@ function manifestOperation(cwd, scripts) {
192
274
  scripts,
193
275
  devDependencies: { [own.name]: isSelfAdoption(cwd) ? "link:." : own.version }
194
276
  };
195
- if (existsSync2(join2(cwd, "package.json"))) {
277
+ if (existsSync4(join3(cwd, "package.json"))) {
196
278
  return { kind: "merge-json", path: "package.json", value };
197
279
  }
198
280
  return {
@@ -207,7 +289,7 @@ function nxTargetCommand(cwd, target) {
207
289
  let project;
208
290
  try {
209
291
  project = parseJsonc(
210
- readFileSync3(join3(cwd, "project.json"), "utf8"),
292
+ readFileSync3(join4(cwd, "project.json"), "utf8"),
211
293
  "project.json"
212
294
  );
213
295
  } catch {
@@ -237,9 +319,699 @@ function existingCommand(cwd, target) {
237
319
  return fromTarget.ranInModule ? fromTarget.command : rootedForScript(fromTarget.command);
238
320
  }
239
321
 
322
+ // src/core/config/nx-target.ts
323
+ import { existsSync as existsSync5 } from "fs";
324
+ import { join as join5 } from "path";
325
+ function nxTargetOperations(options) {
326
+ const { cwd, targets } = options;
327
+ const names = Object.keys(targets);
328
+ if (names.length === 0) return [];
329
+ const operations = [];
330
+ if (existsSync5(join5(cwd, "project.json"))) {
331
+ operations.push({
332
+ kind: "remove-json-keys",
333
+ path: "project.json",
334
+ keys: names.map((name) => ["targets", name])
335
+ });
336
+ }
337
+ operations.push({
338
+ kind: "merge-json",
339
+ path: "package.json",
340
+ value: { nx: { targets } }
341
+ });
342
+ return operations;
343
+ }
344
+
345
+ // src/core/config/tool-script.ts
346
+ var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
347
+ function isSeparatorAt(index) {
348
+ return index % 2 === 1;
349
+ }
350
+ function invokes(segment, binary) {
351
+ return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
352
+ }
353
+ function isSentinelSegment(segment, roleFlag) {
354
+ const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
355
+ return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
356
+ }
357
+ function composeToolScript(existing, options) {
358
+ const { command } = options;
359
+ if (!existing || existing.trim() === "") return command;
360
+ const { roleFlag } = options;
361
+ const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
362
+ const parts = existing.split(SEGMENT_SEPARATOR);
363
+ const commands = parts.filter((_, index) => !isSeparatorAt(index));
364
+ if (!commands.some(isReplaceable)) return existing;
365
+ let replacedOnce = false;
366
+ const rebuilt = parts.map((part, index) => {
367
+ if (isSeparatorAt(index) || !isReplaceable(part)) return part;
368
+ if (replacedOnce) return null;
369
+ replacedOnce = true;
370
+ return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
371
+ });
372
+ const kept = [];
373
+ for (let index = 0; index < rebuilt.length; index += 1) {
374
+ const part = rebuilt[index];
375
+ if (part === null) {
376
+ if (kept.length > 0) kept.pop();
377
+ continue;
378
+ }
379
+ kept.push(part);
380
+ }
381
+ return kept.join("").trim();
382
+ }
383
+ function keepsOtherCommands(script, sentinelCommand) {
384
+ return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
385
+ }
386
+
387
+ // src/roles/build/config-policy.ts
388
+ var BUILD_CONFIG_FILES = [
389
+ "vite.config.ts",
390
+ "vite.config.mts",
391
+ "vite.config.js",
392
+ "vite.config.mjs"
393
+ ];
394
+ var BUILD_PRESET_SPECIFIER = "@hublo/sentinel/build/react";
395
+ var BUILD_SCRIPT_NAME = "build";
396
+ var SENTINEL_BUILD_COMMAND = "sentinel --run --build";
397
+ var DEV_SCRIPT_NAME = "serve";
398
+ var SENTINEL_DEV_COMMAND = "sentinel --run --dev";
399
+ function buildTarget() {
400
+ return {
401
+ [BUILD_SCRIPT_NAME]: {
402
+ cache: true,
403
+ inputs: ["default", "^default", ...BUILD_CONFIG_FILES.map((name) => `{projectRoot}/${name}`)]
404
+ }
405
+ };
406
+ }
407
+ var SENTINEL_OWNED_BUILD_PACKAGES = [
408
+ "vite",
409
+ "@vitejs/plugin-react",
410
+ "@tailwindcss/vite",
411
+ "vite-plugin-svgr",
412
+ "nitro"
413
+ ];
414
+
415
+ // src/roles/build/dev-script.ts
416
+ var CHAIN = " -- ";
417
+ var VITE_TOKEN = /(^|\s|\/)vite(\s|$)/;
418
+ var DEV_SUBCOMMANDS = /* @__PURE__ */ new Set(["dev", "serve"]);
419
+ function appOptions(rest) {
420
+ const tokens = (rest ?? "").trim().split(/\s+/).filter(Boolean);
421
+ const kept = tokens.filter((token, at2) => !(at2 === 0 && DEV_SUBCOMMANDS.has(token)));
422
+ return kept.join(" ");
423
+ }
424
+ function composeDevScript(existing, command) {
425
+ if (existing === void 0 || existing.trim() === "") return command;
426
+ const chunks = existing.trim().split(CHAIN);
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|$)/, "");
430
+ const options = appOptions(rest);
431
+ chunks[at2] = options === "" ? command : `${command} -- ${options}`;
432
+ return chunks.join(CHAIN);
433
+ }
434
+
435
+ // src/roles/build/read-adoption.ts
436
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
437
+ import { join as join6 } from "path";
438
+ var NOT_ADOPTED = (configFile, unreadable = null) => ({
439
+ configFile,
440
+ preset: null,
441
+ adopted: false,
442
+ conformant: false,
443
+ drift: [],
444
+ ownDeclarations: [],
445
+ unreadable
446
+ });
447
+ function buildConfigFile(cwd) {
448
+ return BUILD_CONFIG_FILES.find((name) => existsSync6(join6(cwd, name)));
449
+ }
450
+ function importsSpecifier(source, specifier) {
451
+ const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
452
+ return new RegExp(`(?:from|import)\\s*\\(?\\s*['"\`]${escaped}(?:/[^'"\`]*)?['"\`]`).test(source);
453
+ }
454
+ function importsPreset(source) {
455
+ return importsSpecifier(source, BUILD_PRESET_SPECIFIER);
456
+ }
457
+ var REACT_PLUGIN_SPECIFIERS = ["@vitejs/plugin-react", "@tanstack/react-start"];
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) {
464
+ const configFile = buildConfigFile(cwd);
465
+ if (!configFile) return void 0;
466
+ try {
467
+ return readFileSync4(join6(cwd, configFile), "utf8");
468
+ } catch {
469
+ return void 0;
470
+ }
471
+ }
472
+ function readBuildAdoption(cwd) {
473
+ const configFile = buildConfigFile(cwd);
474
+ if (!configFile) return NOT_ADOPTED(null);
475
+ let source;
476
+ try {
477
+ source = readFileSync4(join6(cwd, configFile), "utf8");
478
+ } catch (error) {
479
+ return NOT_ADOPTED(configFile, error instanceof Error ? error.message : String(error));
480
+ }
481
+ if (!importsPreset(source)) return NOT_ADOPTED(configFile);
482
+ const manifest = readProjectPackageJson(cwd);
483
+ const declared = { ...manifest.dependencies, ...manifest.devDependencies };
484
+ const ownDeclarations = SENTINEL_OWNED_BUILD_PACKAGES.filter((name) => name in declared).map(
485
+ (name) => ({ name, version: declared[name] })
486
+ );
487
+ return {
488
+ configFile,
489
+ preset: "react",
490
+ adopted: true,
491
+ conformant: ownDeclarations.length === 0,
492
+ drift: ownDeclarations.map(
493
+ ({ name, version }) => `declares ${name}@${version} of its own; sentinel owns it, and two copies in one build give the plugins a different Vite than the one running them`
494
+ ),
495
+ ownDeclarations,
496
+ unreadable: null
497
+ };
498
+ }
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
+
528
+ // src/roles/build/plan.ts
529
+ function scaffold() {
530
+ return `import { defineConfig, reactApp } from '${BUILD_PRESET_SPECIFIER}'
531
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
532
+ import { tanstackRouter } from '@tanstack/router-plugin/vite'
533
+ import path from 'node:path'
534
+
535
+ /*
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.
538
+ *
539
+ * TanStack is passed IN rather than imported by sentinel, because the app codes against it
540
+ * directly. \`alias\` is passed through verbatim and never read, so nothing in it can be lost.
541
+ * Anything sentinel does not set goes in \`overrides\`, which merges over the preset last.
542
+ */
543
+ export default defineConfig(({ mode }) =>
544
+ reactApp({
545
+ root: __dirname,
546
+ mode,
547
+ base: '/',
548
+ port: 3000,
549
+ // Explicit, NOT port + 1: the three apps in this repo disagree, one of them goes down.
550
+ hmrPort: 3001,
551
+ router: {
552
+ routesDirectory: path.resolve(__dirname, 'src/routes'),
553
+ generatedRouteTree: path.resolve(__dirname, 'src/routeTree.gen.ts'),
554
+ },
555
+ tanstack: { start: tanstackStart, router: tanstackRouter },
556
+ alias: [],
557
+ }),
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.
563
+ `;
564
+ }
565
+ function ownedBuildDependencies(cwd) {
566
+ const manifest = readProjectPackageJson(cwd);
567
+ const keys = [];
568
+ for (const section of ["dependencies", "devDependencies"]) {
569
+ const declared = manifest[section];
570
+ if (!declared) continue;
571
+ for (const name of SENTINEL_OWNED_BUILD_PACKAGES) {
572
+ if (name in declared) keys.push([section, name]);
573
+ }
574
+ }
575
+ return keys;
576
+ }
577
+ function invokesVite(segment) {
578
+ return /(^|\s|\/)vite(\s|$)/.test(segment.trim());
579
+ }
580
+ function buildScripts(cwd) {
581
+ const own = selfCommand(cwd, "--run --build");
582
+ const scripts = {
583
+ [BUILD_SCRIPT_NAME]: composeToolScript(existingCommand(cwd, BUILD_SCRIPT_NAME), {
584
+ command: own ?? SENTINEL_BUILD_COMMAND,
585
+ replaces: invokesVite,
586
+ // Only meaningful for the self case: it lets a re-init correct a sentinel invocation
587
+ // whose FORM is wrong, without matching another role's script.
588
+ roleFlag: own === void 0 ? void 0 : "--build"
589
+ })
590
+ };
591
+ const existingDev = existingCommand(cwd, DEV_SCRIPT_NAME);
592
+ if (existingDev !== void 0) {
593
+ scripts[DEV_SCRIPT_NAME] = composeDevScript(
594
+ existingDev,
595
+ selfCommand(cwd, "--run --dev") ?? SENTINEL_DEV_COMMAND
596
+ );
597
+ }
598
+ return scripts;
599
+ }
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
+ }
609
+ if (declaredBuildPreset(context.cwd) === void 0 && buildConfigFile(context.cwd) !== void 0) {
610
+ return {
611
+ operations: [],
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.`
613
+ };
614
+ }
615
+ const notes = [];
616
+ const operations = [];
617
+ const configFile = buildConfigFile(context.cwd);
618
+ if (configFile === void 0) {
619
+ operations.push({ kind: "write", path: BUILD_CONFIG_FILES[0], contents: scaffold() });
620
+ notes.push(
621
+ `wrote ${BUILD_CONFIG_FILES[0]} as a starting point. Fill in this app's own values (base, ports, routes); sentinel owns the composition, not the data.`
622
+ );
623
+ } else if (!readsPreset(context.cwd, configFile)) {
624
+ notes.push(
625
+ `${configFile} is this app's own, so it was left alone. To adopt: import \`reactApp\` from \`${BUILD_PRESET_SPECIFIER}\` and pass it this app's values, keeping \`alias\` and your own plugins verbatim. \`sentinel --inspect --build\` reports what the resolved config departs from and what it adds, before and after.`
626
+ );
627
+ }
628
+ operations.push(manifestOperation(context.cwd, buildScripts(context.cwd)));
629
+ operations.push(...nxTargetOperations({ cwd: context.cwd, targets: buildTarget() }));
630
+ const owned = ownedBuildDependencies(context.cwd);
631
+ if (owned.length > 0) {
632
+ operations.push({ kind: "remove-json-keys", path: "package.json", keys: owned });
633
+ notes.push(
634
+ `removed ${owned.map(([, name]) => name).join(", ")} from this module: sentinel owns them now, and two copies of Vite in one build give the plugins a different Vite than the one running them.`
635
+ );
636
+ }
637
+ return { operations, notes };
638
+ }
639
+ function readsPreset(cwd, configFile) {
640
+ if (!existsSync7(join7(cwd, configFile))) return false;
641
+ try {
642
+ return readFileSync5(join7(cwd, configFile), "utf8").includes(BUILD_PRESET_SPECIFIER);
643
+ } catch {
644
+ return false;
645
+ }
646
+ }
647
+
648
+ // src/roles/build/resolve-vite.ts
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" };
654
+ }
655
+ function viteOrigin(cwd) {
656
+ return resolveViteWith(cwd).origin;
657
+ }
658
+
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 {
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
+ */
685
+ appliesTo(preset) {
686
+ return preset === "react" || preset === "svelte";
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
+ */
695
+ declaredPreset(cwd) {
696
+ return declaredBuildPreset(cwd);
697
+ }
698
+ /**
699
+ * The same plan for both targets, deliberately.
700
+ *
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.
704
+ */
705
+ plan(context) {
706
+ return plan(context);
707
+ }
708
+ async status(ctx) {
709
+ const { adopted, preset, conformant, drift, unreadable } = readBuildAdoption(ctx.cwd);
710
+ return { adopted, preset, conformant, drift, unreadable };
711
+ }
712
+ /**
713
+ * Adoption, the binary, and the warning about which copy of Vite is running.
714
+ *
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.
719
+ */
720
+ prepare(ctx) {
721
+ if (!readBuildAdoption(ctx.cwd).adopted) {
722
+ this.say(
723
+ `no sentinel preset in this module's Vite config; run \`sentinel --init --build\` to adopt.`
724
+ );
725
+ return { kind: "stop", result: { ok: true, code: 0 } };
726
+ }
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.`
731
+ );
732
+ return { kind: "stop", result: { ok: false, code: 1 } };
733
+ }
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.`
737
+ );
738
+ }
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
+ });
756
+ if (result.error) {
757
+ this.say(`could not run vite (${result.error.message})`);
758
+ return { ok: false, code: 1 };
759
+ }
760
+ const code = result.status ?? whenSignalled;
761
+ return { ok: code === 0, code };
762
+ }
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
+ `);
767
+ }
768
+ };
769
+
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
+ };
793
+
794
+ // src/roles/build/owned-paths.ts
795
+ var PRESET_OWNED_KEYS = [
796
+ "base",
797
+ "root",
798
+ "define",
799
+ "server",
800
+ "build",
801
+ "nitro",
802
+ "resolve",
803
+ "plugins"
804
+ ];
805
+ function describeDepartures(config, defaults) {
806
+ const departures = [];
807
+ const build = asRecord(config.build);
808
+ const target = build?.target;
809
+ if (typeof target === "string" && target !== defaults.target) {
810
+ departures.push({
811
+ rule: "build.target",
812
+ reason: `${target}, where the convention is ${defaults.target}`
813
+ });
814
+ }
815
+ const server = asRecord(config.server);
816
+ const hosts = server?.allowedHosts;
817
+ if (Array.isArray(hosts) && !sameStrings(hosts, defaults.allowedHosts)) {
818
+ departures.push({
819
+ rule: "server.allowedHosts",
820
+ reason: `${JSON.stringify(hosts)}, where the convention is ${JSON.stringify(defaults.allowedHosts)}`
821
+ });
822
+ }
823
+ return departures;
824
+ }
825
+ function appAdditions(config) {
826
+ const owned = new Set(PRESET_OWNED_KEYS);
827
+ return Object.keys(config).filter((key) => !owned.has(key)).sort();
828
+ }
829
+ function asRecord(value) {
830
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
831
+ }
832
+ function sameStrings(a, b) {
833
+ return a.length === b.length && a.every((value, at2) => value === b[at2]);
834
+ }
835
+
836
+ // src/roles/build/prerequisite.ts
837
+ import { spawnSync as spawnSync2 } from "child_process";
838
+ var PREBUILD_SCRIPT_NAME = `pre${BUILD_SCRIPT_NAME}`;
839
+ function insideBuildScript(env = process.env) {
840
+ return env.npm_lifecycle_event === BUILD_SCRIPT_NAME;
841
+ }
842
+ function runPrerequisite(cwd, env = process.env) {
843
+ const scripts = moduleScripts(cwd);
844
+ const script = scripts[PREBUILD_SCRIPT_NAME];
845
+ if (script === void 0 || script.trim() === "") return { kind: "none" };
846
+ if (insideBuildScript(env)) return { kind: "already-run" };
847
+ const result = spawnSync2("pnpm", ["run", PREBUILD_SCRIPT_NAME], { cwd, stdio: "inherit" });
848
+ if (result.error) {
849
+ return { kind: "failed", code: 1, reason: result.error.message };
850
+ }
851
+ const code = result.status ?? 1;
852
+ if (code !== 0) {
853
+ return {
854
+ kind: "failed",
855
+ code,
856
+ reason: `\`pnpm run ${PREBUILD_SCRIPT_NAME}\` exited ${code}`
857
+ };
858
+ }
859
+ return { kind: "ran" };
860
+ }
861
+
862
+ // src/roles/build/resolve-config.ts
863
+ async function resolveBuildConfig(cwd) {
864
+ try {
865
+ const { loadConfigFromFile } = await import("vite");
866
+ const loaded = await loadConfigFromFile(
867
+ { command: "build", mode: "production" },
868
+ void 0,
869
+ cwd,
870
+ "silent"
871
+ );
872
+ if (!loaded) return { kind: "failed", reason: "Vite found no config file in this module" };
873
+ return { kind: "loaded", config: loaded.config, from: loaded.path };
874
+ } catch (error) {
875
+ return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
876
+ }
877
+ }
878
+
879
+ // src/roles/build/adapters/vite/vite.adapter.ts
880
+ var ViteAdapter = class extends ViteRoleAdapter {
881
+ target = "build";
882
+ get label() {
883
+ return "build";
884
+ }
885
+ /**
886
+ * Build the module.
887
+ *
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.
890
+ */
891
+ async run(ctx) {
892
+ const ready = this.prepare(ctx);
893
+ if (ready.kind === "stop") return ready.result;
894
+ const prerequisite = runPrerequisite(ctx.cwd);
895
+ if (prerequisite.kind === "failed") {
896
+ this.say(
897
+ `the ${PREBUILD_SCRIPT_NAME} step failed, so the build was not started: ${prerequisite.reason}`
898
+ );
899
+ return { ok: false, code: prerequisite.code };
900
+ }
901
+ return this.spawnVite(ctx, ready.vite, ["build"], 1);
902
+ }
903
+ /**
904
+ * What this module builds with, without building it.
905
+ *
906
+ * `--inspect` reads the committed config for every role, and the moment `--build` was wired
907
+ * it joined the sweep — so leaving this to throw would have broken `sentinel --inspect` on
908
+ * every React module, for a role that had only just arrived. Cheap and honest is the bar.
909
+ *
910
+ * `vite` is the interesting field and the one nothing else reports: an adopted config takes
911
+ * its plugins from sentinel, so a module answering `module` here is one build away from the
912
+ * two-copies failure, and this is where that is visible before it happens.
913
+ *
914
+ * The resolved config comes from Vite's own loader rather than from parsing the file. See
915
+ * `resolve-config` for why that is the only honest answer here, and what it costs.
916
+ */
917
+ async inspect(ctx) {
918
+ const adoption = readBuildAdoption(ctx.cwd);
919
+ const base = {
920
+ runner: "vite",
921
+ configFile: adoption.configFile,
922
+ ...adoption.unreadable ? { unreadable: adoption.unreadable } : {},
923
+ vite: viteOrigin(ctx.cwd),
924
+ // Rendered as a labelled block by the shared renderer, the same shape the lint role's
925
+ // parked rules and the format role's overrides already use.
926
+ overrides: adoption.ownDeclarations.map(({ name, version }) => ({
927
+ rule: name,
928
+ reason: `declared by this module at ${version}, where sentinel owns it`
929
+ })),
930
+ prerequisite: moduleScripts(ctx.cwd)[PREBUILD_SCRIPT_NAME] ?? null
931
+ };
932
+ if (adoption.configFile === null) return base;
933
+ const resolved = await resolveBuildConfig(ctx.cwd);
934
+ if (resolved.kind === "failed") {
935
+ return { ...base, configError: resolved.reason };
936
+ }
937
+ const config = resolved.config;
938
+ return {
939
+ ...base,
940
+ // What the app departs from, and what it adds, are different facts. A DEPARTURE is a
941
+ // disagreement with an opinion the preset holds; an ADDITION is the app needing
942
+ // something the preset never claimed, which is the preset working rather than being
943
+ // worked around.
944
+ departures: describeDepartures(config, REACT_APP_DEFAULTS),
945
+ additions: appAdditions(config),
946
+ resolved: {
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,
951
+ // The one number nothing else reports and that no formula predicts: career goes DOWN
952
+ // to 9998 where the others go up, which is why it is data rather than `port + 1`.
953
+ hmrPort: at(config, "server", "hmr", "port") ?? null,
954
+ // Counted, not listed. It is 56-67% of every config today, and printing 296 entries
955
+ // would bury everything above it. The count is what tells you whether it moved.
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"))
960
+ }
961
+ };
962
+ }
963
+ /**
964
+ * Adoption as data, WITHOUT building.
965
+ *
966
+ * Deliberately not "run the build and attach metrics". `--report` is what a migration
967
+ * dashboard calls across the whole workspace, and a bundler is the one tool here where doing
968
+ * the real work costs minutes per module rather than seconds. A report that nobody can
969
+ * afford to run is a report nobody runs.
970
+ *
971
+ * Bundle size is the metric this will eventually want, and it needs a build to produce. It
972
+ * belongs behind an explicit opt-in rather than in the default sweep, and the ticket parks
973
+ * it for exactly that reason.
974
+ */
975
+ async report(ctx) {
976
+ const adoption = readBuildAdoption(ctx.cwd);
977
+ return {
978
+ ok: true,
979
+ code: 0,
980
+ metrics: {
981
+ vite: viteOrigin(ctx.cwd),
982
+ ownBuildDependencies: adoption.ownDeclarations.length
983
+ }
984
+ };
985
+ }
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
+ }
998
+
999
+ // src/roles/build/register.ts
1000
+ function registerBuild() {
1001
+ register(new ViteAdapter());
1002
+ setDefaultRunner("build", "vite");
1003
+ register(new ViteDevAdapter());
1004
+ setDefaultRunner("dev", "vite");
1005
+ }
1006
+
1007
+ // src/roles/format/adapters/oxfmt/oxfmt.adapter.ts
1008
+ import { spawnSync as spawnSync3 } from "child_process";
1009
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
1010
+ import { join as join14 } from "path";
1011
+
240
1012
  // src/core/config/has-source.ts
241
1013
  import { readdirSync } from "fs";
242
- import { extname, join as join4 } from "path";
1014
+ import { extname, join as join8, relative } from "path";
243
1015
  var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
244
1016
  "node_modules",
245
1017
  "dist",
@@ -290,7 +1062,7 @@ function hasSourceFiles(cwd, extensions) {
290
1062
  }
291
1063
  for (const entry of entries) {
292
1064
  if (entry.isDirectory()) {
293
- if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join4(dir, entry.name));
1065
+ if (!SKIP_DIRECTORIES.has(entry.name)) queue.push(join8(dir, entry.name));
294
1066
  continue;
295
1067
  }
296
1068
  if (wanted.has(extname(entry.name))) return true;
@@ -298,46 +1070,34 @@ function hasSourceFiles(cwd, extensions) {
298
1070
  }
299
1071
  return false;
300
1072
  }
301
-
302
- // src/core/config/nx-target.ts
303
- import { existsSync as existsSync3 } from "fs";
304
- import { join as join5 } from "path";
305
- function nxTargetOperations(options) {
306
- const { cwd, targets } = options;
307
- const names = Object.keys(targets);
308
- if (names.length === 0) return [];
309
- const operations = [];
310
- if (existsSync3(join5(cwd, "project.json"))) {
311
- operations.push({
312
- kind: "remove-json-keys",
313
- path: "project.json",
314
- keys: names.map((name) => ["targets", name])
315
- });
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
+ }
316
1093
  }
317
- operations.push({
318
- kind: "merge-json",
319
- path: "package.json",
320
- value: { nx: { targets } }
321
- });
322
- return operations;
1094
+ return found;
323
1095
  }
324
1096
 
325
- // src/core/config/tool-args.ts
326
- import { existsSync as existsSync4 } from "fs";
327
- import { isAbsolute, resolve as resolve2 } from "path";
328
- function splitToolArgs(cwd, toolArgs = [], { valueFlags = [] } = {}) {
329
- const takesValue = new Set(valueFlags);
330
- const options = [];
331
- const paths = [];
332
- let previousTakesValue = false;
333
- for (const arg of toolArgs) {
334
- const looksLikeOption = arg.startsWith("-");
335
- const target = isAbsolute(arg) ? arg : resolve2(cwd, arg);
336
- if (!previousTakesValue && !looksLikeOption && existsSync4(target)) paths.push(arg);
337
- else options.push(arg);
338
- previousTakesValue = !arg.includes("=") && takesValue.has(arg);
339
- }
340
- return { options, paths };
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] : [];
341
1101
  }
342
1102
 
343
1103
  // src/core/settings.ts
@@ -345,8 +1105,8 @@ var WORKSPACE_ROOT_MARKER = "nx.json";
345
1105
  var DEFAULT_MAX_DIAGNOSTICS = 100;
346
1106
 
347
1107
  // src/core/workspace-prep.ts
348
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync } from "fs";
349
- import { dirname as dirname2, join as join6, relative } from "path";
1108
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
1109
+ import { dirname as dirname3, join as join9, relative as relative2 } from "path";
350
1110
  var OVERRIDE_KEY = "i18next>typescript";
351
1111
  var NATIVE_TS_ALIAS = "@typescript/native";
352
1112
  var WORKSPACE_YAML = "pnpm-workspace.yaml";
@@ -355,8 +1115,8 @@ var LIST_ITEM = /^(\s*)-\s+(.*?)\s*$/;
355
1115
  function findWorkspaceRoot(startDir) {
356
1116
  let dir = startDir;
357
1117
  for (; ; ) {
358
- if (existsSync5(join6(dir, WORKSPACE_ROOT_MARKER))) return dir;
359
- const parent = dirname2(dir);
1118
+ if (existsSync8(join9(dir, WORKSPACE_ROOT_MARKER))) return dir;
1119
+ const parent = dirname3(dir);
360
1120
  if (parent === dir) return void 0;
361
1121
  dir = parent;
362
1122
  }
@@ -368,9 +1128,9 @@ function declaredNativeTs(pkg) {
368
1128
  return version || void 0;
369
1129
  }
370
1130
  function ensureI18nextSingleton(root, dryRun) {
371
- const pkgPath = join6(root, "package.json");
372
- if (!existsSync5(pkgPath)) return void 0;
373
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
1131
+ const pkgPath = join9(root, "package.json");
1132
+ if (!existsSync8(pkgPath)) return void 0;
1133
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
374
1134
  const want = declaredNativeTs(pkg);
375
1135
  if (!want) return void 0;
376
1136
  const have = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
@@ -383,10 +1143,10 @@ function ensureI18nextSingleton(root, dryRun) {
383
1143
  return `pinned ${OVERRIDE_KEY} to ${want} in package.json (i18next singleton)`;
384
1144
  }
385
1145
  function ensureReleaseAgeAllowList(root, dryRun) {
386
- const yamlPath = join6(root, WORKSPACE_YAML);
387
- if (!existsSync5(yamlPath)) return void 0;
1146
+ const yamlPath = join9(root, WORKSPACE_YAML);
1147
+ if (!existsSync8(yamlPath)) return void 0;
388
1148
  const own = readOwnPackage().name;
389
- const lines = readFileSync4(yamlPath, "utf8").split("\n");
1149
+ const lines = readFileSync6(yamlPath, "utf8").split("\n");
390
1150
  const keyIdx = lines.findIndex((line) => line.replace(/\s+$/, "") === `${RELEASE_AGE_KEY}:`);
391
1151
  if (keyIdx === -1) return void 0;
392
1152
  let lastItemIdx = keyIdx;
@@ -419,13 +1179,13 @@ var ROOT_PRETTIER_CONFIGS = [
419
1179
  "prettier.config.mjs"
420
1180
  ];
421
1181
  function ensureFormatterExclusion(root, moduleDir, dryRun) {
422
- const rel = relative(root, moduleDir).replaceAll("\\", "/");
1182
+ const rel = relative2(root, moduleDir).replaceAll("\\", "/");
423
1183
  if (rel === "" || rel.startsWith("..")) return void 0;
424
- const ignorePath = join6(root, PRETTIER_IGNORE);
425
- const hasPrettier = existsSync5(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync5(join6(root, name)));
1184
+ const ignorePath = join9(root, PRETTIER_IGNORE);
1185
+ const hasPrettier = existsSync8(ignorePath) || ROOT_PRETTIER_CONFIGS.some((name) => existsSync8(join9(root, name)));
426
1186
  if (!hasPrettier) return void 0;
427
1187
  const pattern = `/${rel}/`;
428
- const existing = existsSync5(ignorePath) ? readFileSync4(ignorePath, "utf8") : "";
1188
+ const existing = existsSync8(ignorePath) ? readFileSync6(ignorePath, "utf8") : "";
429
1189
  const lines = existing.split("\n");
430
1190
  if (lines.some((line) => line.trim().replace(/\/$/, "") === pattern.replace(/\/$/, ""))) {
431
1191
  return void 0;
@@ -461,10 +1221,10 @@ function ensureWorkspacePrep(opts) {
461
1221
  function inspectWorkspacePrep(root) {
462
1222
  const entries = [];
463
1223
  let pkg = {};
464
- const pkgPath = join6(root, "package.json");
465
- if (existsSync5(pkgPath)) {
1224
+ const pkgPath = join9(root, "package.json");
1225
+ if (existsSync8(pkgPath)) {
466
1226
  try {
467
- pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
1227
+ pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
468
1228
  } catch {
469
1229
  pkg = {};
470
1230
  }
@@ -478,9 +1238,9 @@ function inspectWorkspacePrep(root) {
478
1238
  });
479
1239
  }
480
1240
  const own = readOwnPackage().name;
481
- const yamlPath = join6(root, WORKSPACE_YAML);
482
- if (existsSync5(yamlPath)) {
483
- const yaml = readFileSync4(yamlPath, "utf8");
1241
+ const yamlPath = join9(root, WORKSPACE_YAML);
1242
+ if (existsSync8(yamlPath)) {
1243
+ const yaml = readFileSync6(yamlPath, "utf8");
484
1244
  const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
485
1245
  if (listed) {
486
1246
  const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
@@ -493,39 +1253,6 @@ function inspectWorkspacePrep(root) {
493
1253
  return entries;
494
1254
  }
495
1255
 
496
- // src/shared/resolve-bin.ts
497
- import { existsSync as existsSync6 } from "fs";
498
- import { createRequire } from "module";
499
- import { delimiter, dirname as dirname3, join as join7 } from "path";
500
- var require2 = createRequire(import.meta.url);
501
- function resolveBin(fromDir, name) {
502
- let dir = fromDir;
503
- for (; ; ) {
504
- const candidate = join7(dir, "node_modules", ".bin", name);
505
- if (existsSync6(candidate)) return candidate;
506
- const parent = dirname3(dir);
507
- if (parent === dir) return void 0;
508
- dir = parent;
509
- }
510
- }
511
- function binFromOwnInstall(packageName, binName) {
512
- try {
513
- const manifest = require2.resolve(`${packageName}/package.json`);
514
- const bin = require2(manifest).bin;
515
- const relative3 = typeof bin === "string" ? bin : bin?.[binName];
516
- if (!relative3) return void 0;
517
- const executable = join7(dirname3(manifest), relative3);
518
- return existsSync6(executable) ? executable : void 0;
519
- } catch {
520
- return void 0;
521
- }
522
- }
523
- function binSearchPath(cwd) {
524
- return [`${cwd}${delimiter}node_modules/.bin (and parents)`, "sentinel's own install"].join(
525
- " then "
526
- );
527
- }
528
-
529
1256
  // src/roles/format/config-policy.ts
530
1257
  var FORMAT_CONFIG_FILE = ".oxfmtrc.json";
531
1258
  var FORMAT_SCRIPT_NAME = "format";
@@ -572,48 +1299,6 @@ function formatTargets() {
572
1299
  };
573
1300
  }
574
1301
 
575
- // src/core/config/tool-script.ts
576
- var SEGMENT_SEPARATOR = /(\s*(?:&&|\|\||;|&|\|)\s*)/;
577
- function isSeparatorAt(index) {
578
- return index % 2 === 1;
579
- }
580
- function invokes(segment, binary) {
581
- return new RegExp(String.raw`(^|[\s/])${binary}(\s|$)`).test(segment.trim());
582
- }
583
- function isSentinelSegment(segment, roleFlag) {
584
- const runsSentinel = invokes(segment, "sentinel") || segment.includes("bin/sentinel.js");
585
- return runsSentinel && new RegExp(String.raw`(^|\s)${roleFlag}(\s|$)`).test(segment);
586
- }
587
- function composeToolScript(existing, options) {
588
- const { command } = options;
589
- if (!existing || existing.trim() === "") return command;
590
- const { roleFlag } = options;
591
- const isReplaceable = (segment) => options.replaces(segment) || roleFlag !== void 0 && isSentinelSegment(segment, roleFlag);
592
- const parts = existing.split(SEGMENT_SEPARATOR);
593
- const commands = parts.filter((_, index) => !isSeparatorAt(index));
594
- if (!commands.some(isReplaceable)) return existing;
595
- let replacedOnce = false;
596
- const rebuilt = parts.map((part, index) => {
597
- if (isSeparatorAt(index) || !isReplaceable(part)) return part;
598
- if (replacedOnce) return null;
599
- replacedOnce = true;
600
- return options.keepFix && /(^|\s)--fix(\s|$)/.test(part) ? `${command} --fix` : command;
601
- });
602
- const kept = [];
603
- for (let index = 0; index < rebuilt.length; index += 1) {
604
- const part = rebuilt[index];
605
- if (part === null) {
606
- if (kept.length > 0) kept.pop();
607
- continue;
608
- }
609
- kept.push(part);
610
- }
611
- return kept.join("").trim();
612
- }
613
- function keepsOtherCommands(script, sentinelCommand) {
614
- return script.split(SEGMENT_SEPARATOR).filter((_, index) => !isSeparatorAt(index)).some((segment) => segment.trim() !== "" && segment.trim() !== sentinelCommand);
615
- }
616
-
617
1302
  // src/roles/format/format-script.ts
618
1303
  var SENTINEL_FORMAT_COMMAND = "sentinel --run --format";
619
1304
  function isPrettierSegment(segment) {
@@ -642,8 +1327,8 @@ function writesWhenRewritten(name, command) {
642
1327
  var CHECK_SCRIPT_NAMES = /* @__PURE__ */ new Set(["lint", "format", "check", "test", "typecheck", "verify"]);
643
1328
 
644
1329
  // src/roles/format/inherited-ignores.ts
645
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
646
- import { join as join8 } from "path";
1330
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1331
+ import { join as join10 } from "path";
647
1332
  var ROOT_IGNORE_FILE = ".prettierignore";
648
1333
  function isPattern(line) {
649
1334
  const trimmed = line.trim();
@@ -658,11 +1343,11 @@ function toModulePattern(pattern) {
658
1343
  }
659
1344
  function inheritedIgnorePatterns(workspaceRoot) {
660
1345
  if (!workspaceRoot) return [];
661
- const path = join8(workspaceRoot, ROOT_IGNORE_FILE);
662
- if (!existsSync7(path)) return [];
1346
+ const path = join10(workspaceRoot, ROOT_IGNORE_FILE);
1347
+ if (!existsSync9(path)) return [];
663
1348
  let contents;
664
1349
  try {
665
- contents = readFileSync5(path, "utf8");
1350
+ contents = readFileSync7(path, "utf8");
666
1351
  } catch {
667
1352
  return [];
668
1353
  }
@@ -749,13 +1434,13 @@ function formatPresetFor(preset) {
749
1434
  }
750
1435
 
751
1436
  // src/roles/format/prettier-config.ts
752
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
753
- import { join as join10 } from "path";
1437
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1438
+ import { join as join12 } from "path";
754
1439
 
755
1440
  // src/roles/format/resolve-oxfmt.ts
756
- import { readFileSync as readFileSync6 } from "fs";
1441
+ import { readFileSync as readFileSync8 } from "fs";
757
1442
  import { createRequire as createRequire2 } from "module";
758
- import { dirname as dirname4, join as join9 } from "path";
1443
+ import { dirname as dirname4, join as join11 } from "path";
759
1444
  function resolveOxfmt(cwd) {
760
1445
  return resolveBin(cwd, "oxfmt") ?? binFromOwnInstall("oxfmt", "oxfmt");
761
1446
  }
@@ -784,8 +1469,8 @@ function configSchema() {
784
1469
  function readConfigSchema() {
785
1470
  try {
786
1471
  const manifest = createRequire2(import.meta.url).resolve("oxfmt/package.json");
787
- const schemaPath = join9(dirname4(manifest), "configuration_schema.json");
788
- return JSON.parse(readFileSync6(schemaPath, "utf8"));
1472
+ const schemaPath = join11(dirname4(manifest), "configuration_schema.json");
1473
+ return JSON.parse(readFileSync8(schemaPath, "utf8"));
789
1474
  } catch {
790
1475
  return void 0;
791
1476
  }
@@ -816,14 +1501,14 @@ function toOxfmtOverrides(value) {
816
1501
  return { overrides, unresolved };
817
1502
  }
818
1503
  function readPrettierSettings(cwd) {
819
- const file = PRETTIER_CONFIG_FILES.find((name) => existsSync8(join10(cwd, name)));
1504
+ const file = PRETTIER_CONFIG_FILES.find((name) => existsSync10(join12(cwd, name)));
820
1505
  if (!file) return { options: {}, unresolved: [] };
821
1506
  if (/\.(js|cjs|mjs)$/.test(file)) {
822
1507
  return { options: {}, file, unresolved: [`${file} is JavaScript, so it was not executed`] };
823
1508
  }
824
1509
  let parsed;
825
1510
  try {
826
- parsed = parseJsonc(readFileSync7(join10(cwd, file), "utf8"), file);
1511
+ parsed = parseJsonc(readFileSync9(join12(cwd, file), "utf8"), file);
827
1512
  } catch {
828
1513
  return { options: {}, file, unresolved: [`${file} could not be parsed`] };
829
1514
  }
@@ -853,9 +1538,9 @@ function readPrettierSettings(cwd) {
853
1538
  }
854
1539
 
855
1540
  // src/roles/format/read-adoption.ts
856
- import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
857
- import { join as join11 } from "path";
858
- var NOT_ADOPTED = (configFile, unreadable = null) => ({
1541
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
1542
+ import { join as join13 } from "path";
1543
+ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
859
1544
  configFile,
860
1545
  preset: null,
861
1546
  adopted: false,
@@ -870,19 +1555,19 @@ function sameValue(a, b) {
870
1555
  return JSON.stringify(a) === JSON.stringify(b);
871
1556
  }
872
1557
  function readFormatAdoption(cwd) {
873
- const path = join11(cwd, FORMAT_CONFIG_FILE);
874
- if (!existsSync9(path)) return NOT_ADOPTED(null);
1558
+ const path = join13(cwd, FORMAT_CONFIG_FILE);
1559
+ if (!existsSync11(path)) return NOT_ADOPTED2(null);
875
1560
  let parsed;
876
1561
  try {
877
- parsed = parseJsonc(readFileSync8(path, "utf8"), FORMAT_CONFIG_FILE);
1562
+ parsed = parseJsonc(readFileSync10(path, "utf8"), FORMAT_CONFIG_FILE);
878
1563
  } catch (error) {
879
1564
  const reason = error instanceof Error ? error.message : String(error);
880
- return NOT_ADOPTED(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
1565
+ return NOT_ADOPTED2(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
881
1566
  }
882
1567
  const provenance = parsed[PROVENANCE_KEY];
883
- if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED(FORMAT_CONFIG_FILE);
1568
+ if (!provenance || typeof provenance.preset !== "string") return NOT_ADOPTED2(FORMAT_CONFIG_FILE);
884
1569
  if (!hasFormatPreset(provenance.preset)) {
885
- return NOT_ADOPTED(
1570
+ return NOT_ADOPTED2(
886
1571
  FORMAT_CONFIG_FILE,
887
1572
  `${FORMAT_CONFIG_FILE} declares the format preset "${provenance.preset}", which sentinel does not ship (shipped: ${FORMAT_PRESETS.join(", ")}). Re-run \`sentinel --init --format\`.`
888
1573
  );
@@ -999,7 +1684,7 @@ var OxfmtAdapter = class extends BaseAdapter {
999
1684
  manifestOperation(context.cwd, this.formatScripts(context.cwd))
1000
1685
  ];
1001
1686
  const prettierConfigs = PRETTIER_CONFIG_FILES.filter(
1002
- (name) => existsSync10(join12(context.cwd, name))
1687
+ (name) => existsSync12(join14(context.cwd, name))
1003
1688
  );
1004
1689
  for (const name of prettierConfigs) operations.push({ kind: "delete", path: name });
1005
1690
  const removableDeps = this.modulePrettierDependencies(context.cwd);
@@ -1058,7 +1743,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1058
1743
  async afterInit(ctx) {
1059
1744
  const oxfmt = resolveOxfmt(ctx.cwd);
1060
1745
  if (!oxfmt) return;
1061
- const pass = spawnSync(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1746
+ const pass = spawnSync3(oxfmt, ["--write", "."], { cwd: ctx.cwd, encoding: "utf8" });
1062
1747
  process.stderr.write(` ${palette(process.stderr).dim(FORMATTER_DIFFERENCES)}
1063
1748
  `);
1064
1749
  if (pass.status === 0) return;
@@ -1121,10 +1806,14 @@ var OxfmtAdapter = class extends BaseAdapter {
1121
1806
  const { options, paths } = splitToolArgs(ctx.cwd, ctx.toolArgs, {
1122
1807
  valueFlags: OXFMT_VALUE_FLAGS
1123
1808
  });
1124
- const result = spawnSync(oxfmt, [mode, ...options, ...paths.length > 0 ? paths : ["."]], {
1125
- cwd: ctx.cwd,
1126
- stdio: "inherit"
1127
- });
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
+ );
1128
1817
  if (result.error) {
1129
1818
  process.stderr.write(
1130
1819
  `sentinel format(oxfmt): could not run oxfmt (${result.error.message})
@@ -1199,7 +1888,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1199
1888
  );
1200
1889
  return { ok: false, code: 1, metrics: { ...base, unformatted: null } };
1201
1890
  }
1202
- const result = spawnSync(oxfmt, ["--list-different", "."], { cwd: ctx.cwd, encoding: "utf8" });
1891
+ const result = spawnSync3(oxfmt, ["--list-different", "."], { cwd: ctx.cwd, encoding: "utf8" });
1203
1892
  if (result.error) {
1204
1893
  process.stderr.write(
1205
1894
  `sentinel format(oxfmt): could not run oxfmt (${result.error.message})
@@ -1235,7 +1924,7 @@ var OxfmtAdapter = class extends BaseAdapter {
1235
1924
  modulePrettierDependencies(cwd) {
1236
1925
  const isPrettierPackage = (name) => name === "prettier" || name.startsWith("prettier-plugin-") || name.startsWith("@prettier/");
1237
1926
  try {
1238
- const manifest = JSON.parse(readFileSync9(join12(cwd, "package.json"), "utf8"));
1927
+ const manifest = JSON.parse(readFileSync11(join14(cwd, "package.json"), "utf8"));
1239
1928
  return Object.keys(manifest.devDependencies ?? {}).filter(isPrettierPackage).map((name) => ["devDependencies", name]);
1240
1929
  } catch {
1241
1930
  return [];
@@ -1315,30 +2004,139 @@ function registerFormat() {
1315
2004
  setDefaultRunner("format", "oxfmt");
1316
2005
  }
1317
2006
 
1318
- // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1319
- import { spawnSync as spawnSync2 } from "child_process";
1320
- import { existsSync as existsSync17, readFileSync as readFileSync16, rmSync, writeFileSync as writeFileSync2 } from "fs";
1321
- import { join as join20 } from "path";
1322
-
1323
- // src/core/config/deferred-rules.ts
1324
- function deferredRuleNames(rules) {
1325
- return rules.map((entry) => entry.rule);
2007
+ // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
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";
2011
+
2012
+ // src/core/config/deferred-rules.ts
2013
+ function deferredRuleNames(rules) {
2014
+ return rules.map((entry) => entry.rule);
2015
+ }
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
+ }
1326
2122
  }
1327
2123
 
1328
2124
  // src/roles/lint/config-policy.ts
1329
2125
  var LINT_CONFIG_FILE = ".oxlintrc.json";
1330
2126
  var LINT_SCRIPT_NAME = "lint";
1331
- var PRESET_DIR = "./node_modules/@hublo/sentinel/oxlint";
2127
+ var PRESET_DIR = "./node_modules/@hublo/sentinel/lint";
1332
2128
  function presetPath(preset) {
1333
2129
  return `${PRESET_DIR}/${preset}.json`;
1334
2130
  }
1335
2131
  function isSentinelPreset(entry) {
1336
- return entry.includes("@hublo/sentinel/oxlint/");
2132
+ return /@hublo\/sentinel\/(lint|oxlint)\//.test(entry);
1337
2133
  }
1338
2134
  function extendsWithPreset(current, variant) {
1339
2135
  const own = presetPath(variant);
1340
- const others = current.filter((entry) => entry !== own && !isSentinelPreset(entry));
1341
- 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];
1342
2140
  }
1343
2141
  function presetVariant(preset, cwd) {
1344
2142
  const normalised = cwd.replaceAll("\\", "/");
@@ -1347,7 +2145,7 @@ function presetVariant(preset, cwd) {
1347
2145
  function presetOfVariant(variant) {
1348
2146
  return variant === "react-lib" ? "react" : variant;
1349
2147
  }
1350
- var SENTINEL_LINT_PRESET = /@hublo\/sentinel\/oxlint\/([a-z-]+)\.json$/;
2148
+ var SENTINEL_LINT_PRESET = /@hublo\/sentinel\/(?:lint|oxlint)\/([a-z-]+)\.json$/;
1351
2149
  function presetNameFromPath(preset) {
1352
2150
  return preset ? SENTINEL_LINT_PRESET.exec(preset)?.[1] ?? void 0 : void 0;
1353
2151
  }
@@ -1362,46 +2160,6 @@ function lintTarget() {
1362
2160
  return { cache: true, inputs: ["default", "^default", `{projectRoot}/${LINT_CONFIG_FILE}`] };
1363
2161
  }
1364
2162
 
1365
- // src/roles/lint/presets/base.json
1366
- var base_default2 = {
1367
- rules: {
1368
- "no-var": "error",
1369
- "prefer-const": [
1370
- "error",
1371
- {
1372
- destructuring: "any",
1373
- ignoreReadBeforeAssign: false
1374
- }
1375
- ],
1376
- "prefer-rest-params": "error",
1377
- "prefer-spread": "error",
1378
- "typescript/ban-ts-comment": "error",
1379
- "typescript/no-array-constructor": "error",
1380
- "typescript/no-duplicate-enum-values": "error",
1381
- "typescript/no-empty-object-type": "error",
1382
- "typescript/no-extra-non-null-assertion": "error",
1383
- "typescript/no-misused-new": "error",
1384
- "typescript/no-namespace": "error",
1385
- "typescript/no-non-null-asserted-optional-chain": "error",
1386
- "typescript/no-this-alias": "error",
1387
- "typescript/no-unnecessary-type-constraint": "error",
1388
- "typescript/no-unsafe-declaration-merging": "error",
1389
- "typescript/no-unsafe-function-type": "error",
1390
- "typescript/no-unused-expressions": [
1391
- "error",
1392
- {
1393
- allowShortCircuit: false,
1394
- allowTaggedTemplates: false,
1395
- allowTernary: false
1396
- }
1397
- ],
1398
- "typescript/no-wrapper-object-types": "error",
1399
- "typescript/prefer-as-const": "error",
1400
- "typescript/prefer-namespace-keyword": "error",
1401
- "typescript/triple-slash-reference": "error"
1402
- }
1403
- };
1404
-
1405
2163
  // src/roles/lint/presets/nest.json
1406
2164
  var nest_default = {
1407
2165
  plugins: ["typescript", "jest", "import", "unicorn", "oxc", "node"],
@@ -1741,6 +2499,7 @@ var nest_default = {
1741
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.",
1742
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.",
1743
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.",
1744
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.",
1745
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.",
1746
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.",
@@ -2694,10 +3453,51 @@ var react_default = {
2694
3453
  uncovered: {
2695
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.",
2696
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.",
2697
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."
2698
3458
  }
2699
3459
  };
2700
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
+
2701
3501
  // src/roles/lint/presets/svelte.json
2702
3502
  var svelte_default = {
2703
3503
  plugins: ["typescript", "oxc"],
@@ -3036,7 +3836,7 @@ var tools_default = {
3036
3836
  };
3037
3837
 
3038
3838
  // src/roles/lint/preset-data.ts
3039
- var BASE_RULES = base_default2.rules;
3839
+ var SHARED_RULES = shared_default.rules;
3040
3840
  var PRESET_FILES = {
3041
3841
  nest: nest_default,
3042
3842
  node: node_default,
@@ -3058,15 +3858,18 @@ function hasLintPreset(preset) {
3058
3858
  function presetFile(preset) {
3059
3859
  return VARIANT_FILES[preset] ?? PRESET_FILES[preset] ?? node_default;
3060
3860
  }
3061
- function baseRules() {
3062
- return BASE_RULES;
3861
+ function sharedRules() {
3862
+ return SHARED_RULES;
3063
3863
  }
3064
3864
  function presetRules(preset) {
3065
3865
  return presetFile(preset).rules;
3066
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
+ }
3067
3870
 
3068
3871
  // src/roles/lint/rule-data.ts
3069
- var baseRules2 = baseRules();
3872
+ var sharedRules2 = sharedRules();
3070
3873
  function isDecision(value) {
3071
3874
  return typeof value === "object" && value !== null && !Array.isArray(value);
3072
3875
  }
@@ -3093,7 +3896,7 @@ function validate(preset, rules) {
3093
3896
  }
3094
3897
  }
3095
3898
  function layered(preset) {
3096
- const base = baseRules();
3899
+ const base = sharedRules();
3097
3900
  const layer = presetRules(preset);
3098
3901
  validate("base", base);
3099
3902
  validate(preset, layer);
@@ -3132,63 +3935,14 @@ function downgradedRulesFor(preset) {
3132
3935
  }
3133
3936
 
3134
3937
  // src/roles/lint/extra-layers.ts
3135
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3136
- import { isAbsolute as isAbsolute2, join as join13, resolve as resolve3 } from "path";
3137
-
3138
- // src/roles/lint/module-baseline.ts
3139
- var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
3140
- var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
3141
- var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
3142
- function holdsFrom(errorCounts) {
3143
- return [...errorCounts].map(([rule, count]) => ({ rule, count })).sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule));
3144
- }
3145
- function renderBaseline(holds, version) {
3146
- const entries = holds.map(({ rule }) => ` ${JSON.stringify(rule)}: "warn"`).join(",\n");
3147
- return [
3148
- "{",
3149
- ` // Written by @hublo/sentinel@${version}. Do not edit by hand: \`sentinel --init --lint\``,
3150
- " // regenerates it from a fresh measurement, and a rule you have since fixed drops out",
3151
- " // and returns to `error` on its own.",
3152
- " //",
3153
- " // These rules are held at `warn` because this module ALREADY violated them when it",
3154
- " // adopted. They still run and still report, so the build cannot fail on them.",
3155
- " //",
3156
- " // The hold is per RULE, not per occurrence: oxlint cannot freeze a specific list of",
3157
- " // violations, so a NEW violation of one of these rules is covered too, and will only",
3158
- " // warn. That is the cost of adopting without a red build, and the reason to clear this",
3159
- " // file rather than live with it.",
3160
- " //",
3161
- " // `sentinel --inspect --lint` counts what is left. Fix them and re-run `--init --lint`",
3162
- " // to get the preset severity back.",
3163
- ' "rules": {',
3164
- entries,
3165
- " }",
3166
- "}",
3167
- ""
3168
- ].join("\n");
3169
- }
3170
- function extendsWithBaseline(current, hasHolds) {
3171
- const withoutBaseline = current.filter((entry) => entry !== LINT_BASELINE_SPECIFIER);
3172
- return hasHolds ? [...withoutBaseline, LINT_BASELINE_SPECIFIER] : withoutBaseline;
3173
- }
3174
- function describeHolds(holds) {
3175
- if (holds.length === 0) return "";
3176
- const total = holds.reduce((sum, hold) => sum + hold.count, 0);
3177
- const listed = holds.slice(0, 5).map(({ rule, count }) => `${rule} (${count})`).join(", ");
3178
- const rest = holds.length > 5 ? `, and ${holds.length - 5} more` : "";
3179
- 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`;
3180
- }
3181
-
3182
- // src/roles/lint/extra-layers.ts
3183
- function isSentinelPreset2(entry) {
3184
- return entry.includes("@hublo/sentinel/oxlint/");
3185
- }
3938
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
3939
+ import { isAbsolute as isAbsolute2, join as join17, resolve as resolve3 } from "path";
3186
3940
  function ruleCount(cwd, specifier) {
3187
3941
  const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
3188
- if (!existsSync11(path)) return void 0;
3942
+ if (!existsSync14(path)) return void 0;
3189
3943
  try {
3190
3944
  const parsed = parseJsonc(
3191
- readFileSync10(path, "utf8"),
3945
+ readFileSync13(path, "utf8"),
3192
3946
  specifier
3193
3947
  );
3194
3948
  return Object.keys(parsed.rules ?? {}).length;
@@ -3197,7 +3951,7 @@ function ruleCount(cwd, specifier) {
3197
3951
  }
3198
3952
  }
3199
3953
  function extraLayers(cwd, extendsList) {
3200
- return extendsList.filter((entry) => !isSentinelPreset2(entry) && entry !== LINT_BASELINE_SPECIFIER).map((entry) => {
3954
+ return extendsList.filter((entry) => !isSentinelPreset(entry) && entry !== LINT_BASELINE_SPECIFIER).map((entry) => {
3201
3955
  const count = ruleCount(cwd, entry);
3202
3956
  return {
3203
3957
  rule: entry,
@@ -3208,7 +3962,7 @@ function extraLayers(cwd, extendsList) {
3208
3962
  function committedExtendsList(cwd, configFile) {
3209
3963
  try {
3210
3964
  const parsed = parseJsonc(
3211
- readFileSync10(join13(cwd, configFile), "utf8"),
3965
+ readFileSync13(join17(cwd, configFile), "utf8"),
3212
3966
  configFile
3213
3967
  );
3214
3968
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -3267,8 +4021,8 @@ function errorCountsByConfigRule(stdout) {
3267
4021
  }
3268
4022
 
3269
4023
  // src/core/config/read-adoption.ts
3270
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
3271
- import { join as join15 } from "path";
4024
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
4025
+ import { join as join19 } from "path";
3272
4026
 
3273
4027
  // src/core/config/owned-keys.ts
3274
4028
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -3283,12 +4037,12 @@ function localOnlyKeys(config, permitted, presetSets) {
3283
4037
  }
3284
4038
 
3285
4039
  // src/core/config/resolve-config-target.ts
3286
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3287
- import { join as join14 } from "path";
4040
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
4041
+ import { join as join18 } from "path";
3288
4042
  function readExtends(absolutePath) {
3289
4043
  let parsed;
3290
4044
  try {
3291
- parsed = parseJsonc(readFileSync11(absolutePath, "utf8"), absolutePath);
4045
+ parsed = parseJsonc(readFileSync14(absolutePath, "utf8"), absolutePath);
3292
4046
  } catch {
3293
4047
  return [];
3294
4048
  }
@@ -3302,8 +4056,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
3302
4056
  let existing;
3303
4057
  let existingExtendsSomething = false;
3304
4058
  for (const candidate of candidates) {
3305
- const absolutePath = join14(moduleDir, candidate);
3306
- if (!existsSync12(absolutePath)) continue;
4059
+ const absolutePath = join18(moduleDir, candidate);
4060
+ if (!existsSync15(absolutePath)) continue;
3307
4061
  const chain = readExtends(absolutePath);
3308
4062
  if (existing === void 0) {
3309
4063
  existing = candidate;
@@ -3326,7 +4080,7 @@ function normaliseExtends(value) {
3326
4080
  }
3327
4081
  return [];
3328
4082
  }
3329
- var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
4083
+ var NOT_ADOPTED3 = (configFile, unreadable = null) => ({
3330
4084
  configFile,
3331
4085
  preset: null,
3332
4086
  adopted: false,
@@ -3336,18 +4090,18 @@ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
3336
4090
  });
3337
4091
  function readAdoption(cwd, options) {
3338
4092
  const target = resolveConfigTarget(cwd, options);
3339
- if (target.reason === "none" || !existsSync13(join15(cwd, target.path))) {
3340
- return NOT_ADOPTED2(target.reason === "none" ? null : target.path);
4093
+ if (target.reason === "none" || !existsSync16(join19(cwd, target.path))) {
4094
+ return NOT_ADOPTED3(target.reason === "none" ? null : target.path);
3341
4095
  }
3342
4096
  let parsed;
3343
4097
  try {
3344
- parsed = parseJsonc(readFileSync12(join15(cwd, target.path), "utf8"), target.path);
4098
+ parsed = parseJsonc(readFileSync15(join19(cwd, target.path), "utf8"), target.path);
3345
4099
  } catch (error) {
3346
4100
  const reason = error instanceof Error ? error.message : String(error);
3347
- return NOT_ADOPTED2(target.path, `${target.path} could not be parsed (${reason})`);
4101
+ return NOT_ADOPTED3(target.path, `${target.path} could not be parsed (${reason})`);
3348
4102
  }
3349
4103
  const preset = normaliseExtends(parsed.extends).find((entry) => options.presetPattern.test(entry)) ?? null;
3350
- if (preset === null) return NOT_ADOPTED2(target.path);
4104
+ if (preset === null) return NOT_ADOPTED3(target.path);
3351
4105
  const settings = options.settingsKey === null ? parsed : parsed[options.settingsKey];
3352
4106
  const drift = presetOwnedKeys(settings, options.permitted, options.presetOwns?.(preset));
3353
4107
  return {
@@ -3375,9 +4129,9 @@ function readLintAdoption(cwd) {
3375
4129
  }
3376
4130
 
3377
4131
  // src/roles/lint/resolve-oxlint.ts
3378
- import { existsSync as existsSync14 } from "fs";
4132
+ import { existsSync as existsSync17 } from "fs";
3379
4133
  import { createRequire as createRequire3 } from "module";
3380
- import { delimiter as delimiter2, dirname as dirname5, join as join16 } from "path";
4134
+ import { delimiter as delimiter2, dirname as dirname5, join as join20 } from "path";
3381
4135
  import { fileURLToPath as fileURLToPath2 } from "url";
3382
4136
  var PACKAGE_OF = {
3383
4137
  oxlint: "oxlint",
@@ -3402,30 +4156,30 @@ function tsgolintShim(cwd) {
3402
4156
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
3403
4157
  try {
3404
4158
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
3405
- candidates.push(join16(packageDir, "node_modules", ".bin", "tsgolint"));
3406
- candidates.push(join16(packageDir, "..", ".bin", "tsgolint"));
4159
+ candidates.push(join20(packageDir, "node_modules", ".bin", "tsgolint"));
4160
+ candidates.push(join20(packageDir, "..", ".bin", "tsgolint"));
3407
4161
  } catch {
3408
4162
  }
3409
4163
  }
3410
4164
  try {
3411
4165
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
3412
- candidates.push(join16(ownRoot, "node_modules", ".bin", "tsgolint"));
4166
+ candidates.push(join20(ownRoot, "node_modules", ".bin", "tsgolint"));
3413
4167
  } catch {
3414
4168
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
3415
4169
  }
3416
- return candidates.find((candidate) => candidate !== "" && existsSync14(candidate));
4170
+ return candidates.find((candidate) => candidate !== "" && existsSync17(candidate));
3417
4171
  }
3418
4172
  function oxlintSearchPath(cwd) {
3419
4173
  return binSearchPath(cwd);
3420
4174
  }
3421
4175
 
3422
4176
  // src/roles/lint/adapters/oxlint/plan.ts
3423
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
3424
- import { join as join19 } from "path";
4177
+ import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
4178
+ import { join as join23 } from "path";
3425
4179
 
3426
4180
  // src/roles/lint/eslint-ignores.ts
3427
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3428
- import { join as join17 } from "path";
4181
+ import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
4182
+ import { join as join21 } from "path";
3429
4183
  var IGNORE_BLOCKS = [/\bignores\s*:\s*\[([^\]]*)\]/g, /\bglobalIgnores\s*\(\s*\[([^\]]*)\]/g];
3430
4184
  var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
3431
4185
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
@@ -3438,11 +4192,11 @@ function readRootEslintIgnores(root) {
3438
4192
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
3439
4193
  }
3440
4194
  function readEslintIgnores(cwd) {
3441
- const config = ESLINT_CONFIG_FILES.map((name) => join17(cwd, name)).find((path) => existsSync15(path));
4195
+ const config = ESLINT_CONFIG_FILES.map((name) => join21(cwd, name)).find((path) => existsSync18(path));
3442
4196
  if (!config) return { patterns: [], unresolved: [] };
3443
4197
  let source;
3444
4198
  try {
3445
- source = readFileSync13(config, "utf8");
4199
+ source = readFileSync16(config, "utf8");
3446
4200
  } catch {
3447
4201
  return { patterns: [], unresolved: [] };
3448
4202
  }
@@ -3496,8 +4250,8 @@ function lintPresetFor(preset) {
3496
4250
  }
3497
4251
 
3498
4252
  // src/roles/lint/rename-suppressions.ts
3499
- import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync } from "fs";
3500
- import { join as join18, 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";
3501
4255
  var SOURCE_EXTENSIONS = [
3502
4256
  ".ts",
3503
4257
  ".tsx",
@@ -3544,10 +4298,10 @@ function* sourceFiles(dir) {
3544
4298
  return;
3545
4299
  }
3546
4300
  for (const entry of entries) {
3547
- const full = join18(dir, entry);
4301
+ const full = join22(dir, entry);
3548
4302
  let isDirectory;
3549
4303
  try {
3550
- isDirectory = statSync(full).isDirectory();
4304
+ isDirectory = statSync2(full).isDirectory();
3551
4305
  } catch {
3552
4306
  continue;
3553
4307
  }
@@ -3564,7 +4318,7 @@ function findSuppressionRenames(cwd, renames) {
3564
4318
  for (const file of sourceFiles(cwd)) {
3565
4319
  let content;
3566
4320
  try {
3567
- content = readFileSync14(file, "utf8");
4321
+ content = readFileSync17(file, "utf8");
3568
4322
  } catch {
3569
4323
  continue;
3570
4324
  }
@@ -3588,7 +4342,7 @@ function findSuppressionRenames(cwd, renames) {
3588
4342
  }
3589
4343
  if (hits.length === 0) return;
3590
4344
  found.push({
3591
- file: relative2(cwd, file),
4345
+ file: relative3(cwd, file),
3592
4346
  line: index + 1,
3593
4347
  from: line,
3594
4348
  to: line.replace(rules, renamed.join(", ")),
@@ -3643,7 +4397,7 @@ function summariseByPlugin(rules) {
3643
4397
  }
3644
4398
  return [...counts.entries()].sort((left, right) => right[1] - left[1]).map(([plugin, count]) => `${count} ${plugin}`).join(", ");
3645
4399
  }
3646
- function plan(context) {
4400
+ function plan2(context) {
3647
4401
  if (!hasLintPreset(context.preset)) {
3648
4402
  return {
3649
4403
  operations: [],
@@ -3692,7 +4446,7 @@ function plan(context) {
3692
4446
  keys: removableDeps
3693
4447
  });
3694
4448
  }
3695
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync16(join19(context.cwd, name)));
4449
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync19(join23(context.cwd, name)));
3696
4450
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
3697
4451
  operations.push(
3698
4452
  ...nxTargetOperations({
@@ -3790,7 +4544,7 @@ function lintScripts(cwd) {
3790
4544
  function committedExtends(cwd) {
3791
4545
  try {
3792
4546
  const parsed = parseJsonc(
3793
- readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
4547
+ readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
3794
4548
  LINT_CONFIG_FILE
3795
4549
  );
3796
4550
  return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
@@ -3801,7 +4555,7 @@ function committedExtends(cwd) {
3801
4555
  function committedIgnorePatterns(cwd) {
3802
4556
  try {
3803
4557
  const parsed = parseJsonc(
3804
- readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
4558
+ readFileSync18(join23(cwd, LINT_CONFIG_FILE), "utf8"),
3805
4559
  LINT_CONFIG_FILE
3806
4560
  );
3807
4561
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -3813,7 +4567,7 @@ function moduleEslintDependencies(cwd) {
3813
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/");
3814
4568
  let manifest;
3815
4569
  try {
3816
- manifest = JSON.parse(readFileSync15(join19(cwd, "package.json"), "utf8"));
4570
+ manifest = JSON.parse(readFileSync18(join23(cwd, "package.json"), "utf8"));
3817
4571
  } catch {
3818
4572
  return [];
3819
4573
  }
@@ -3872,23 +4626,37 @@ var OxlintAdapter = class extends BaseAdapter {
3872
4626
  * lines. The adapter stays the contract with the engine.
3873
4627
  */
3874
4628
  plan(context) {
3875
- return plan(context);
4629
+ return plan2(context);
3876
4630
  }
3877
4631
  declaredPreset(cwd) {
3878
4632
  return presetOfVariant(presetNameFromPath(readLintAdoption(cwd).preset));
3879
4633
  }
3880
4634
  /**
3881
- * The autofix pass. Output is captured rather than streamed: the fix run is noise, and the
3882
- * 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.
3883
4646
  */
3884
4647
  fixPass(ctx, oxlint, env) {
3885
- const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, "--fix", "."], {
4648
+ const before = snapshotFiles(ctx.cwd, LINTABLE_EXTENSIONS);
4649
+ const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, "--fix", "."], {
3886
4650
  cwd: ctx.cwd,
3887
4651
  encoding: "utf8",
3888
4652
  env,
3889
4653
  maxBuffer: CAPTURE_MAX_BUFFER
3890
4654
  });
3891
- 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 };
3892
4660
  }
3893
4661
  /** The module's current `lint` script, so adoption replaces only the eslint part of it. */
3894
4662
  /**
@@ -3911,7 +4679,8 @@ var OxlintAdapter = class extends BaseAdapter {
3911
4679
  const oxlint = resolveOxlint(ctx.cwd);
3912
4680
  if (!oxlint) return;
3913
4681
  const env = { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) };
3914
- this.fixPass(ctx, oxlint, env);
4682
+ process.stderr.write(` ${describeFixOutcome(this.fixPass(ctx, oxlint, env), "lint")}
4683
+ `);
3915
4684
  this.writeModuleBaseline(ctx, oxlint, env);
3916
4685
  }
3917
4686
  /**
@@ -3932,20 +4701,20 @@ var OxlintAdapter = class extends BaseAdapter {
3932
4701
  * build the developer can see, rather than a silent half-adoption they cannot.
3933
4702
  */
3934
4703
  writeModuleBaseline(ctx, oxlint, env) {
3935
- const configPath = join20(ctx.cwd, LINT_CONFIG_FILE);
3936
- const baselinePath = join20(ctx.cwd, LINT_BASELINE_FILE);
3937
- if (!existsSync17(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;
3938
4707
  let config;
3939
4708
  try {
3940
4709
  config = parseJsonc(
3941
- readFileSync16(configPath, "utf8"),
4710
+ readFileSync19(configPath, "utf8"),
3942
4711
  LINT_CONFIG_FILE
3943
4712
  );
3944
4713
  } catch {
3945
4714
  return;
3946
4715
  }
3947
4716
  const current = Array.isArray(config.extends) ? config.extends : [];
3948
- const measurePath = join20(ctx.cwd, LINT_MEASURE_FILE);
4717
+ const measurePath = join24(ctx.cwd, LINT_MEASURE_FILE);
3949
4718
  let measured;
3950
4719
  try {
3951
4720
  writeFileSync2(
@@ -3953,7 +4722,7 @@ var OxlintAdapter = class extends BaseAdapter {
3953
4722
  `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
3954
4723
  `
3955
4724
  );
3956
- measured = spawnSync2(
4725
+ measured = spawnSync4(
3957
4726
  oxlint,
3958
4727
  [
3959
4728
  "-c",
@@ -3986,14 +4755,14 @@ var OxlintAdapter = class extends BaseAdapter {
3986
4755
  } else {
3987
4756
  rmSync(baselinePath, { force: true });
3988
4757
  }
3989
- 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])) {
3990
4759
  config.extends = next;
3991
4760
  writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
3992
4761
  `);
3993
4762
  }
3994
4763
  }
3995
4764
  async run(ctx) {
3996
- if (!existsSync17(join20(ctx.cwd, LINT_CONFIG_FILE))) {
4765
+ if (!existsSync20(join24(ctx.cwd, LINT_CONFIG_FILE))) {
3997
4766
  process.stderr.write(
3998
4767
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
3999
4768
  `
@@ -4038,9 +4807,10 @@ var OxlintAdapter = class extends BaseAdapter {
4038
4807
  valueFlags: OXLINT_VALUE_FLAGS
4039
4808
  });
4040
4809
  const targets = paths.length > 0 ? paths : ["."];
4810
+ const emptyScope = scopeFlags(paths);
4041
4811
  const lint = (extra) => {
4042
- const passed = [...extra, ...passedOptions];
4043
- const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
4812
+ const passed = [...extra, ...emptyScope, ...passedOptions];
4813
+ const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...passed, ...targets], {
4044
4814
  cwd: ctx.cwd,
4045
4815
  stdio: "inherit",
4046
4816
  env
@@ -4078,6 +4848,33 @@ var OxlintAdapter = class extends BaseAdapter {
4078
4848
  rule: entry.rule,
4079
4849
  reason: entry.reason
4080
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
+ })),
4081
4878
  // What this module enforces BEYOND the preset. Not drift, and not covered by anything
4082
4879
  // else here: the stub still holds only `extends` and `ignorePatterns`, so a module with
4083
4880
  // a team layer reports `conformant=true drift=[]` and used to say nothing at all about
@@ -4113,7 +4910,7 @@ var OxlintAdapter = class extends BaseAdapter {
4113
4910
  let stub;
4114
4911
  try {
4115
4912
  stub = parseJsonc(
4116
- readFileSync16(join20(cwd, LINT_CONFIG_FILE), "utf8"),
4913
+ readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
4117
4914
  LINT_CONFIG_FILE
4118
4915
  );
4119
4916
  } catch {
@@ -4123,7 +4920,7 @@ var OxlintAdapter = class extends BaseAdapter {
4123
4920
  for (const entry of stub.extends ?? []) {
4124
4921
  try {
4125
4922
  const preset = parseJsonc(
4126
- readFileSync16(join20(cwd, entry), "utf8"),
4923
+ readFileSync19(join24(cwd, entry), "utf8"),
4127
4924
  entry
4128
4925
  );
4129
4926
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -4140,7 +4937,7 @@ var OxlintAdapter = class extends BaseAdapter {
4140
4937
  const oxlint = resolveOxlint(ctx.cwd);
4141
4938
  if (!oxlint) return { ok: false, code: 1, metrics: { error: "oxlint not found" } };
4142
4939
  const typeAware = canRunTypeAware(ctx.cwd) ? ["--type-aware"] : [];
4143
- const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
4940
+ const result = spawnSync4(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
4144
4941
  cwd: ctx.cwd,
4145
4942
  encoding: "utf8",
4146
4943
  env: { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) },
@@ -4193,14 +4990,14 @@ var OxlintAdapter = class extends BaseAdapter {
4193
4990
  let parsed;
4194
4991
  try {
4195
4992
  parsed = parseJsonc(
4196
- readFileSync16(join20(cwd, LINT_CONFIG_FILE), "utf8"),
4993
+ readFileSync19(join24(cwd, LINT_CONFIG_FILE), "utf8"),
4197
4994
  LINT_CONFIG_FILE
4198
4995
  );
4199
4996
  } catch {
4200
4997
  return void 0;
4201
4998
  }
4202
4999
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
4203
- return targets.find((target) => !existsSync17(join20(cwd, target)));
5000
+ return targets.find((target) => !existsSync20(join24(cwd, target)));
4204
5001
  }
4205
5002
  /** Announce what is not enforced, so reduced coverage is never silent. */
4206
5003
  announceDisabled(preset) {
@@ -4231,36 +5028,10 @@ function registerLint() {
4231
5028
  }
4232
5029
 
4233
5030
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
4234
- import { spawnSync as spawnSync3 } from "child_process";
4235
- import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
5031
+ import { spawnSync as spawnSync5 } from "child_process";
5032
+ import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
4236
5033
  import { createRequire as createRequire4 } from "module";
4237
- import { join as join22 } from "path";
4238
-
4239
- // src/roles/typescript/presets/base.json
4240
- var base_default3 = {
4241
- $schema: "https://json.schemastore.org/tsconfig",
4242
- compilerOptions: {
4243
- strict: true,
4244
- noFallthroughCasesInSwitch: true,
4245
- forceConsistentCasingInFileNames: true,
4246
- esModuleInterop: true,
4247
- skipLibCheck: true
4248
- },
4249
- deferred: {
4250
- noImplicitAny: {
4251
- phase: 2,
4252
- reason: "the implicit-any migration (TS70xx)"
4253
- },
4254
- noUnusedLocals: {
4255
- phase: 2,
4256
- reason: "unused-local cleanup (TS6133)"
4257
- },
4258
- noUnusedParameters: {
4259
- phase: 2,
4260
- reason: "unused-parameter cleanup (TS6133)"
4261
- }
4262
- }
4263
- };
5034
+ import { join as join26 } from "path";
4264
5035
 
4265
5036
  // src/roles/typescript/presets/nest.json
4266
5037
  var nest_default2 = {
@@ -4311,15 +5082,41 @@ var react_default2 = {
4311
5082
  }
4312
5083
  };
4313
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
+
4314
5111
  // src/roles/typescript/preset-data.ts
4315
- var BASE = base_default3;
5112
+ var SHARED = shared_default2;
4316
5113
  var LAYERS = {
4317
5114
  nest: nest_default2,
4318
5115
  node: node_default2,
4319
5116
  react: react_default2
4320
5117
  };
4321
5118
  var SHIPPED_PRESETS = Object.keys(LAYERS).sort();
4322
- 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));
4323
5120
  function validate2() {
4324
5121
  for (const { rule, phase, reason } of DEFERRED_RULES) {
4325
5122
  if (!Number.isInteger(phase) || phase < 2) {
@@ -4332,9 +5129,9 @@ function validate2() {
4332
5129
  `deferred rule "${rule}" has no reason; --inspect reports it and would say nothing`
4333
5130
  );
4334
5131
  }
4335
- if (rule in BASE.compilerOptions) {
5132
+ if (rule in SHARED.compilerOptions) {
4336
5133
  throw new Error(
4337
- `"${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`
4338
5135
  );
4339
5136
  }
4340
5137
  for (const [preset, layer] of Object.entries(LAYERS)) {
@@ -4354,8 +5151,8 @@ function tsPresetFor(preset) {
4354
5151
  const layer = LAYERS[preset];
4355
5152
  if (!layer) throw new Error(`no TypeScript preset ships for preset "${preset}"`);
4356
5153
  return {
4357
- ...BASE.$schema === void 0 ? {} : { $schema: BASE.$schema },
4358
- compilerOptions: { ...BASE.compilerOptions, ...layer.compilerOptions }
5154
+ ...SHARED.$schema === void 0 ? {} : { $schema: SHARED.$schema },
5155
+ compilerOptions: { ...SHARED.compilerOptions, ...layer.compilerOptions }
4359
5156
  };
4360
5157
  }
4361
5158
 
@@ -4398,7 +5195,7 @@ function resolveTsconfigTarget(moduleDir) {
4398
5195
  }
4399
5196
 
4400
5197
  // src/roles/typescript/read-adoption.ts
4401
- var SENTINEL_PRESET = /^@hublo\/sentinel\/tsconfig\/[a-z-]+$/;
5198
+ var SENTINEL_PRESET = /^@hublo\/sentinel\/(?:typescript|tsconfig)\/[a-z-]+$/;
4402
5199
  function readTsconfigAdoption(cwd) {
4403
5200
  return readAdoption(cwd, {
4404
5201
  candidates: TSCONFIG_CANDIDATES,
@@ -4415,8 +5212,8 @@ function readTsconfigAdoption(cwd) {
4415
5212
  }
4416
5213
 
4417
5214
  // src/roles/typescript/adapters/tsc/plan.ts
4418
- import { readFileSync as readFileSync17 } from "fs";
4419
- import { join as join21 } from "path";
5215
+ import { readFileSync as readFileSync20 } from "fs";
5216
+ import { join as join25 } from "path";
4420
5217
 
4421
5218
  // src/roles/typescript/typecheck-script.ts
4422
5219
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -4450,9 +5247,11 @@ function typecheckScripts(cwd) {
4450
5247
  [TYPECHECK_SCRIPT_NAME]: composeTypecheckScript(existingCommand(cwd, TYPECHECK_SCRIPT_NAME))
4451
5248
  };
4452
5249
  }
5250
+ var SENTINEL_TS_PRESET = /@hublo\/sentinel\/(?:typescript|tsconfig)\//;
4453
5251
  function composeExtends(current, preset) {
4454
5252
  const chain = typeof current === "string" ? [current] : Array.isArray(current) ? current.filter((entry) => typeof entry === "string") : [];
4455
- return chain.includes(preset) ? chain : [...chain, preset];
5253
+ const withoutOwn = chain.filter((entry) => entry !== preset && !SENTINEL_TS_PRESET.test(entry));
5254
+ return [...withoutOwn, preset];
4456
5255
  }
4457
5256
  function declaredPreset(cwd) {
4458
5257
  const { preset } = readTsconfigAdoption(cwd);
@@ -4481,7 +5280,7 @@ function planAdoption(context) {
4481
5280
  };
4482
5281
  }
4483
5282
  const target = resolveTsconfigTarget(context.cwd);
4484
- const preset = `@hublo/sentinel/tsconfig/${context.preset}`;
5283
+ const preset = `@hublo/sentinel/typescript/${context.preset}`;
4485
5284
  const addScript = manifestOperation(context.cwd, typecheckScripts(context.cwd));
4486
5285
  const nxTargets = nxTargetOperations({
4487
5286
  cwd: context.cwd,
@@ -4507,7 +5306,7 @@ function planAdoption(context) {
4507
5306
  };
4508
5307
  }
4509
5308
  const existing = parseJsonc(
4510
- readFileSync17(join21(context.cwd, target.path), "utf8"),
5309
+ readFileSync20(join25(context.cwd, target.path), "utf8"),
4511
5310
  target.path
4512
5311
  );
4513
5312
  const extendsChain = composeExtends(existing.extends, preset);
@@ -4545,7 +5344,7 @@ function planAdoption(context) {
4545
5344
 
4546
5345
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
4547
5346
  var SENTINEL_PACKAGE = "@hublo/sentinel";
4548
- var SENTINEL_PRESET_SCOPE = `${SENTINEL_PACKAGE}/tsconfig/`;
5347
+ var SENTINEL_PRESET_SCOPES = [`${SENTINEL_PACKAGE}/typescript/`, `${SENTINEL_PACKAGE}/tsconfig/`];
4549
5348
  var DIAGNOSTIC_RE = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
4550
5349
  function parseDiagnostics(output) {
4551
5350
  const diagnostics = [];
@@ -4645,7 +5444,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4645
5444
  let chain;
4646
5445
  try {
4647
5446
  const parsed = parseJsonc(
4648
- readFileSync18(join22(cwd, target.path), "utf8"),
5447
+ readFileSync21(join26(cwd, target.path), "utf8"),
4649
5448
  target.path
4650
5449
  );
4651
5450
  chain = parsed.extends;
@@ -4654,11 +5453,11 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4654
5453
  }
4655
5454
  const entries = typeof chain === "string" ? [chain] : Array.isArray(chain) ? chain : [];
4656
5455
  const preset = entries.find(
4657
- (entry) => typeof entry === "string" && entry.startsWith(SENTINEL_PRESET_SCOPE)
5456
+ (entry) => typeof entry === "string" && SENTINEL_PRESET_SCOPES.some((scope) => entry.startsWith(scope))
4658
5457
  );
4659
5458
  if (preset === void 0) return void 0;
4660
5459
  try {
4661
- createRequire4(join22(cwd, "noop.js")).resolve(preset);
5460
+ createRequire4(join26(cwd, "noop.js")).resolve(preset);
4662
5461
  return void 0;
4663
5462
  } catch {
4664
5463
  return preset;
@@ -4699,7 +5498,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4699
5498
  return { ok: false, code: 1 };
4700
5499
  }
4701
5500
  if (options.length > 0) return this.runWithOptions(ctx, tsc, config);
4702
- const result = spawnSync3(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
5501
+ const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, stdio: "inherit" });
4703
5502
  if (result.error) {
4704
5503
  process.stderr.write(
4705
5504
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -4734,7 +5533,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4734
5533
  let worst = 0;
4735
5534
  for (const project of this.referencedProjects(ctx.cwd, config)) {
4736
5535
  const args = ["-p", project, "--noEmit", "--composite", "false", ...ctx.toolArgs ?? []];
4737
- const result = spawnSync3(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
5536
+ const result = spawnSync5(tsc, args, { cwd: ctx.cwd, stdio: "inherit" });
4738
5537
  if (result.error) {
4739
5538
  process.stderr.write(
4740
5539
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -4754,7 +5553,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4754
5553
  referencedProjects(cwd, config) {
4755
5554
  try {
4756
5555
  const parsed = parseJsonc(
4757
- readFileSync18(join22(cwd, config), "utf8"),
5556
+ readFileSync21(join26(cwd, config), "utf8"),
4758
5557
  config
4759
5558
  );
4760
5559
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -4770,7 +5569,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4770
5569
  * check.
4771
5570
  */
4772
5571
  typecheckTarget(cwd) {
4773
- if (existsSync18(join22(cwd, "tsconfig.json"))) return "tsconfig.json";
5572
+ if (existsSync21(join26(cwd, "tsconfig.json"))) return "tsconfig.json";
4774
5573
  const target = resolveTsconfigTarget(cwd);
4775
5574
  return target.reason === "none" ? null : target.path;
4776
5575
  }
@@ -4823,7 +5622,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4823
5622
  return { ok: true, code: 0, metrics: _TscAdapter.NOTHING_TO_REPORT };
4824
5623
  }
4825
5624
  const tsc = resolveBin(ctx.cwd, "tsc") ?? "tsc";
4826
- const result = spawnSync3(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
5625
+ const result = spawnSync5(tsc, ["-b", config], { cwd: ctx.cwd, encoding: "utf8" });
4827
5626
  if (result.error) {
4828
5627
  process.stderr.write(
4829
5628
  `sentinel typescript(tsc): could not run tsc (${result.error.message}); is TypeScript installed in the module?
@@ -4866,7 +5665,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4866
5665
  * errors in the output mean the rule must be on.
4867
5666
  */
4868
5667
  noImplicitAnyEnabled(cwd, tsc, config, mainOutput) {
4869
- const shown = spawnSync3(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
5668
+ const shown = spawnSync5(tsc, ["-p", config, "--showConfig"], { cwd, encoding: "utf8" });
4870
5669
  if (shown.status === 0 && shown.stdout) {
4871
5670
  try {
4872
5671
  const co = parseJsonc(
@@ -4892,6 +5691,7 @@ function registerAdapters() {
4892
5691
  registerTypescript();
4893
5692
  registerLint();
4894
5693
  registerFormat();
5694
+ registerBuild();
4895
5695
  }
4896
5696
 
4897
5697
  // src/core/detect-framework.ts
@@ -4973,16 +5773,9 @@ function replaceLines(current, replacements) {
4973
5773
  }
4974
5774
 
4975
5775
  // src/core/apply-plan.ts
4976
- import { existsSync as existsSync19, readFileSync as readFileSync19, 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";
4977
5777
  import { resolve as resolve4, sep } from "path";
4978
5778
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
4979
-
4980
- // src/shared/deep-merge.ts
4981
- function isPlainObject(value) {
4982
- return typeof value === "object" && value !== null && !Array.isArray(value);
4983
- }
4984
-
4985
- // src/core/apply-plan.ts
4986
5779
  function resolveWithinRoot(cwd, relativePath) {
4987
5780
  const root = resolve4(cwd);
4988
5781
  const absolutePath = resolve4(root, relativePath);
@@ -4992,7 +5785,7 @@ function resolveWithinRoot(cwd, relativePath) {
4992
5785
  return absolutePath;
4993
5786
  }
4994
5787
  function readIfExists(absolutePath) {
4995
- return existsSync19(absolutePath) ? readFileSync19(absolutePath, "utf8") : void 0;
5788
+ return existsSync22(absolutePath) ? readFileSync22(absolutePath, "utf8") : void 0;
4996
5789
  }
4997
5790
  function* leaves(value, prefix = []) {
4998
5791
  for (const [key, keyValue] of Object.entries(value)) {
@@ -5073,9 +5866,9 @@ function applyOperationTo(current, operation) {
5073
5866
  }
5074
5867
  }
5075
5868
  }
5076
- function preparePlan(cwd, plan2) {
5869
+ function preparePlan(cwd, plan3) {
5077
5870
  const prepared = /* @__PURE__ */ new Map();
5078
- for (const operation of plan2.operations) {
5871
+ for (const operation of plan3.operations) {
5079
5872
  const absolutePath = resolveWithinRoot(cwd, operation.path);
5080
5873
  const existing = prepared.get(operation.path);
5081
5874
  const before = existing?.before ?? readIfExists(absolutePath) ?? "";
@@ -5097,8 +5890,8 @@ function writeFileAtomic(absolutePath, contents) {
5097
5890
  writeFileSync3(tempPath, contents);
5098
5891
  renameSync(tempPath, absolutePath);
5099
5892
  }
5100
- function applyPlan(cwd, plan2) {
5101
- const changed = preparePlan(cwd, plan2).filter((file) => file.before !== file.after);
5893
+ function applyPlan(cwd, plan3) {
5894
+ const changed = preparePlan(cwd, plan3).filter((file) => file.before !== file.after);
5102
5895
  for (const file of changed) {
5103
5896
  if (file.deleted) {
5104
5897
  rmSync2(file.absolutePath, { force: true });
@@ -5110,8 +5903,8 @@ function applyPlan(cwd, plan2) {
5110
5903
  }
5111
5904
 
5112
5905
  // src/core/config/preset-evidence.ts
5113
- import { existsSync as existsSync20, readdirSync as readdirSync3, readFileSync as readFileSync20 } from "fs";
5114
- import { join as join23 } from "path";
5906
+ import { existsSync as existsSync23, readdirSync as readdirSync3, readFileSync as readFileSync23 } from "fs";
5907
+ import { join as join27 } from "path";
5115
5908
  var PATH_SIGNALS = [
5116
5909
  {
5117
5910
  preset: "nest",
@@ -5125,11 +5918,11 @@ var DEPENDENCY_SIGNALS = [
5125
5918
  { preset: "nest", pattern: /^@nestjs\// }
5126
5919
  ];
5127
5920
  function dependencyNames(cwd) {
5128
- const path = join23(cwd, "package.json");
5129
- if (!existsSync20(path)) return [];
5921
+ const path = join27(cwd, "package.json");
5922
+ if (!existsSync23(path)) return [];
5130
5923
  try {
5131
5924
  const manifest = parseJsonc(
5132
- readFileSync20(path, "utf8"),
5925
+ readFileSync23(path, "utf8"),
5133
5926
  path
5134
5927
  );
5135
5928
  return [
@@ -5152,7 +5945,7 @@ function declaresJsx(cwd) {
5152
5945
  for (const name of entries) {
5153
5946
  try {
5154
5947
  const config = parseJsonc(
5155
- readFileSync20(join23(cwd, name), "utf8"),
5948
+ readFileSync23(join27(cwd, name), "utf8"),
5156
5949
  name
5157
5950
  );
5158
5951
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -5203,14 +5996,14 @@ function resolveFlavour(opts) {
5203
5996
  }
5204
5997
  return detection.preset;
5205
5998
  }
5206
- function previewPlan(opts, plan2) {
5207
- const changed = preparePlan(opts.cwd, plan2).filter((file) => file.before !== file.after);
5999
+ function previewPlan(opts, plan3) {
6000
+ const changed = preparePlan(opts.cwd, plan3).filter((file) => file.before !== file.after);
5208
6001
  if (opts.json) {
5209
6002
  process.stdout.write(
5210
6003
  JSON.stringify(
5211
6004
  {
5212
6005
  dryRun: true,
5213
- notes: plan2.notes ?? [],
6006
+ notes: plan3.notes ?? [],
5214
6007
  files: changed.map(({ path, before, after, deleted }) => ({
5215
6008
  path,
5216
6009
  action: deleted ? "delete" : before.length === 0 ? "create" : "update",
@@ -5225,7 +6018,7 @@ function previewPlan(opts, plan2) {
5225
6018
  return 0;
5226
6019
  }
5227
6020
  process.stderr.write(" dry run: no files written\n");
5228
- for (const note of plan2.notes ?? []) process.stderr.write(` ${note}
6021
+ for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
5229
6022
  `);
5230
6023
  if (changed.length === 0) {
5231
6024
  process.stderr.write(" nothing to change\n");
@@ -5246,7 +6039,8 @@ async function dispatch(opts) {
5246
6039
  throw new Error(`dispatch handles --init only; --${opts.verb} routes through analyse()`);
5247
6040
  }
5248
6041
  const detected = resolveFlavour(opts);
5249
- const adapter = resolve(opts.target, detected, opts.runner);
6042
+ const resolutionPreset = opts.preset ?? declaredPresetFor(opts.target, opts.cwd) ?? detected;
6043
+ const adapter = resolve(opts.target, resolutionPreset, opts.runner);
5250
6044
  const preset = opts.preset ?? adapter.declaredPreset?.(opts.cwd) ?? detected;
5251
6045
  if (opts.preset !== void 0) {
5252
6046
  const contradiction = presetContradiction(opts.preset, opts.cwd);
@@ -5268,25 +6062,25 @@ async function dispatch(opts) {
5268
6062
  }
5269
6063
  }
5270
6064
  const context = { cwd: opts.cwd, preset: effective };
5271
- const plan2 = await adapter.plan(context);
5272
- if (plan2.blocked) {
5273
- process.stderr.write(`sentinel (${opts.target}): ${plan2.blocked}
6065
+ const plan3 = await adapter.plan(context);
6066
+ if (plan3.blocked) {
6067
+ process.stderr.write(`sentinel (${opts.target}): ${plan3.blocked}
5274
6068
  `);
5275
6069
  return 1;
5276
6070
  }
5277
- if (plan2.skipped) {
5278
- process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan2.skipped}`)}
6071
+ if (plan3.skipped) {
6072
+ process.stderr.write(` ${palette(process.stderr).dim(`${opts.target}: ${plan3.skipped}`)}
5279
6073
  `);
5280
6074
  return 0;
5281
6075
  }
5282
6076
  if (opts.dryRun) {
5283
- return previewPlan(opts, plan2);
6077
+ return previewPlan(opts, plan3);
5284
6078
  }
5285
- for (const change of applyPlan(opts.cwd, plan2)) {
6079
+ for (const change of applyPlan(opts.cwd, plan3)) {
5286
6080
  process.stderr.write(` ${change.deleted ? "removed" : "wrote"} ${change.path}
5287
6081
  `);
5288
6082
  }
5289
- for (const note of plan2.notes ?? []) process.stderr.write(` ${note}
6083
+ for (const note of plan3.notes ?? []) process.stderr.write(` ${note}
5290
6084
  `);
5291
6085
  if (adapter.afterInit) {
5292
6086
  process.stderr.write(` fixing what ${opts.target} can fix automatically...
@@ -5303,27 +6097,30 @@ async function dispatch(opts) {
5303
6097
  }
5304
6098
 
5305
6099
  export {
6100
+ PresetUnsupportedError,
5306
6101
  register,
5307
6102
  setDefaultRunner,
5308
6103
  all,
6104
+ declaredPresetFor,
5309
6105
  availableTargets,
5310
6106
  resolve,
5311
6107
  BaseAdapter,
6108
+ resolveBin,
5312
6109
  readOwnVersion,
5313
6110
  readProjectPackageJson,
5314
6111
  readNxProjectName,
5315
6112
  VERBS,
5316
6113
  TARGETS,
6114
+ SWEEPABLE_TARGETS,
5317
6115
  PRESET_NAMES,
5318
6116
  WORKSPACE_ROOT_MARKER,
5319
6117
  findWorkspaceRoot,
5320
6118
  ensureWorkspacePrep,
5321
6119
  inspectWorkspacePrep,
5322
6120
  palette,
5323
- resolveBin,
5324
6121
  registerAdapters,
5325
6122
  describeFramework,
5326
6123
  detectFramework,
5327
6124
  dispatch
5328
6125
  };
5329
- //# sourceMappingURL=chunk-W73ISYMG.js.map
6126
+ //# sourceMappingURL=chunk-FLJ2QNFF.js.map