@hublo/sentinel 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ import {
9
9
  dispatch,
10
10
  ensureWorkspacePrep,
11
11
  findWorkspaceRoot,
12
+ inspectWorkspacePrep,
12
13
  palette,
13
14
  readNxProjectName,
14
15
  readOwnVersion,
@@ -16,7 +17,7 @@ import {
16
17
  registerAdapters,
17
18
  resolve,
18
19
  resolveBin
19
- } from "../chunk-RLVLDC5H.js";
20
+ } from "../chunk-RGIEWANO.js";
20
21
 
21
22
  // bin/sentinel.ts
22
23
  import { program } from "commander";
@@ -438,6 +439,10 @@ function renderStatusSummary(items, p) {
438
439
  }
439
440
 
440
441
  // src/cli/run-verb.ts
442
+ function workspacePrep(cwd2) {
443
+ const root = findWorkspaceRoot(cwd2);
444
+ return root ? inspectWorkspacePrep(root) : [];
445
+ }
441
446
  async function runVerb(ctx) {
442
447
  const { modules, scope } = resolveContext(ctx.cwd, { module: ctx.module, ci: ctx.ci });
443
448
  const out = palette(process.stdout);
@@ -482,7 +487,15 @@ async function runVerb(ctx) {
482
487
  });
483
488
  const summary = generateSummaries(results, ctx.targets);
484
489
  if (ctx.json) {
485
- const base = ctx.verb === "status" || ctx.verb === "inspect" ? { ...summary, coverage: statusCoverage(summary.results) } : summary;
490
+ const base = ctx.verb === "status" || ctx.verb === "inspect" ? {
491
+ ...summary,
492
+ coverage: statusCoverage(summary.results),
493
+ // The root-level changes sentinel maintains, each with its justification checked
494
+ // NOW rather than described in a comment. They are the only thing sentinel does
495
+ // that nobody can see afterwards, and every one of them is temporary. See
496
+ // `inspectWorkspacePrep`.
497
+ ...ctx.verb === "inspect" ? { workspace: workspacePrep(ctx.cwd) } : {}
498
+ } : summary;
486
499
  const payload = ctx.toolArgs.length > 0 ? { ...base, toolArgs: ctx.toolArgs } : base;
487
500
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
488
501
  } else if (ctx.verb === "status" || ctx.verb === "inspect" && modules.length > 1) {
@@ -493,6 +506,19 @@ async function runVerb(ctx) {
493
506
  process.stdout.write(renderSummary(item, out) + "\n");
494
507
  }
495
508
  }
509
+ if (!ctx.json && ctx.verb === "inspect") {
510
+ const prep = workspacePrep(ctx.cwd);
511
+ if (prep.length > 0) {
512
+ process.stdout.write(`
513
+ sentinel maintains these at the workspace root:
514
+ `);
515
+ for (const entry of prep) {
516
+ process.stdout.write(` ${out.strong(entry.rule)}
517
+ ${entry.reason}
518
+ `);
519
+ }
520
+ }
521
+ }
496
522
  process.stderr.write(
497
523
  err.dim(` ${modules.length} module(s) [${scope}] in ${Date.now() - started}ms
498
524
  `)
@@ -458,6 +458,40 @@ function ensureWorkspacePrep(opts) {
458
458
  opts.formattedModule === void 0 ? void 0 : ensureFormatterExclusion(opts.root, opts.formattedModule, dryRun)
459
459
  ].filter((message) => message !== void 0);
460
460
  }
461
+ function inspectWorkspacePrep(root) {
462
+ const entries = [];
463
+ let pkg = {};
464
+ const pkgPath = join6(root, "package.json");
465
+ if (existsSync5(pkgPath)) {
466
+ try {
467
+ pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
468
+ } catch {
469
+ pkg = {};
470
+ }
471
+ }
472
+ const pinned = pkg.pnpm?.overrides?.[OVERRIDE_KEY];
473
+ if (pinned !== void 0) {
474
+ const stillForks = declaredNativeTs(pkg);
475
+ entries.push({
476
+ rule: `package.json \u2192 pnpm.overrides.${OVERRIDE_KEY}`,
477
+ reason: stillForks === void 0 ? `pinned to ${pinned}, but the root no longer declares ${NATIVE_TS_ALIAS}, so nothing forks any more. This override is now dead config and can be removed.` : `pinned to ${pinned}, matching the second TypeScript the root declares (${NATIVE_TS_ALIAS}). It re-resolves i18next for EVERY module that depends on it, not only adopted ones. It retires with the second TypeScript.`
478
+ });
479
+ }
480
+ const own = readOwnPackage().name;
481
+ const yamlPath = join6(root, WORKSPACE_YAML);
482
+ if (existsSync5(yamlPath)) {
483
+ const yaml = readFileSync4(yamlPath, "utf8");
484
+ const listed = new RegExp(`^\\s*-\\s*['"]?${own.replace("/", "\\/")}['"]?\\s*$`, "m").test(yaml);
485
+ if (listed) {
486
+ const gated = new RegExp(`^\\s*minimumReleaseAge\\s*:`, "m").test(yaml);
487
+ entries.push({
488
+ rule: `${WORKSPACE_YAML} \u2192 ${RELEASE_AGE_KEY}: ${own}`,
489
+ reason: gated ? `${own} is exempt from this workspace's release-age gate, so a freshly published version installs instead of being held. That is a standing exemption from a supply-chain control: the exposure is one bump wide, since the version is pinned and the lockfile carries its integrity, so a re-publish of the same version does not pass. Retire it once a bump can simply wait out the window.` : `${own} is allow-listed, but this workspace no longer sets minimumReleaseAge, so there is no gate to be exempt from. This entry is now dead config and can be removed.`
490
+ });
491
+ }
492
+ }
493
+ return entries;
494
+ }
461
495
 
462
496
  // src/shared/resolve-bin.ts
463
497
  import { existsSync as existsSync6 } from "fs";
@@ -514,6 +548,7 @@ var PERMITTED_LOCAL_KEYS = [
514
548
  "experimentalOperatorPosition"
515
549
  ];
516
550
  var ALWAYS_LOCAL_KEYS = ["ignorePatterns", "overrides"];
551
+ var FORMAT_DEFAULT_IGNORES = ["**/*.toml"];
517
552
  var PRETTIER_CONFIG_FILES = [
518
553
  ".prettierrc",
519
554
  ".prettierrc.json",
@@ -926,7 +961,9 @@ var OxfmtAdapter = class extends BaseAdapter {
926
961
  const localKeys = Object.keys(local).filter((key) => !ALWAYS_LOCAL_KEYS.includes(key)).sort();
927
962
  const inherited = inheritedIgnorePatterns(findWorkspaceRoot(context.cwd));
928
963
  const declaredIgnores = Array.isArray(local.ignorePatterns) ? local.ignorePatterns.filter((v) => typeof v === "string") : [];
929
- const ignorePatterns = [.../* @__PURE__ */ new Set([...declaredIgnores, ...inherited])].sort();
964
+ const ignorePatterns = [
965
+ .../* @__PURE__ */ new Set([...declaredIgnores, ...inherited, ...FORMAT_DEFAULT_IGNORES])
966
+ ].sort();
930
967
  if (ignorePatterns.length > 0) local.ignorePatterns = ignorePatterns;
931
968
  const unchanged = existing.adopted && existing.conformant;
932
969
  const version = unchanged && existing.presetVersion ? existing.presetVersion : own.version;
@@ -1264,8 +1301,8 @@ function registerFormat() {
1264
1301
 
1265
1302
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1266
1303
  import { spawnSync as spawnSync2 } from "child_process";
1267
- import { existsSync as existsSync16, readFileSync as readFileSync15, rmSync, writeFileSync as writeFileSync2 } from "fs";
1268
- import { join as join19 } from "path";
1304
+ import { existsSync as existsSync17, readFileSync as readFileSync16, rmSync, writeFileSync as writeFileSync2 } from "fs";
1305
+ import { join as join20 } from "path";
1269
1306
 
1270
1307
  // src/core/config/deferred-rules.ts
1271
1308
  function deferredRuleNames(rules) {
@@ -1279,6 +1316,14 @@ var PRESET_DIR = "./node_modules/@hublo/sentinel/oxlint";
1279
1316
  function presetPath(preset) {
1280
1317
  return `${PRESET_DIR}/${preset}.json`;
1281
1318
  }
1319
+ function isSentinelPreset(entry) {
1320
+ return entry.includes("@hublo/sentinel/oxlint/");
1321
+ }
1322
+ function extendsWithPreset(current, variant) {
1323
+ const own = presetPath(variant);
1324
+ const others = current.filter((entry) => entry !== own && !isSentinelPreset(entry));
1325
+ return [own, ...new Set(others)];
1326
+ }
1282
1327
  function presetVariant(preset, cwd) {
1283
1328
  const normalised = cwd.replaceAll("\\", "/");
1284
1329
  return preset === "react" && /(^|\/)libs\//.test(normalised) ? "react-lib" : preset;
@@ -1298,7 +1343,7 @@ var ESLINT_CONFIG_FILES = [
1298
1343
  "eslint.config.ts"
1299
1344
  ];
1300
1345
  function lintTarget() {
1301
- return { cache: true, inputs: ["default", `{projectRoot}/${LINT_CONFIG_FILE}`] };
1346
+ return { cache: true, inputs: ["default", "^default", `{projectRoot}/${LINT_CONFIG_FILE}`] };
1302
1347
  }
1303
1348
 
1304
1349
  // src/roles/lint/presets/base.json
@@ -1642,8 +1687,8 @@ var nest_default = {
1642
1687
  },
1643
1688
  "typescript/return-await": {
1644
1689
  severity: "warn",
1645
- reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. The `never` option matches what no-return-await meant, so this is like-for-like, but it now runs over every file and finds real violations (9 on bff-admin, 5 on host-admin, measured with --type-aware). Held at warn so adoption does not turn a green module red, and raised to error once they are fixed. Switching to the stricter `in-try-catch`, which typescript-eslint recommends because `return await` inside try IS needed for correct error handling, is a separate decision with its own count.",
1646
- options: ["never"]
1690
+ reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. `in-try-catch` rather than `never`, and the planning adoption is what settled it. `never` forces `return await f()` to become `const x = await f(); return x` even inside a `try`, where the await is REQUIRED for the catch to fire: it produced 10 such edits across 15 modules, every one of them inside a try/catch, and every one of them a rewrite that made the code longer without making it safer. Worse, the obvious reading of the rule is to delete the `await`, which silently stops the catch from running. `in-try-catch` is what typescript-eslint recommends and flags none of those 10. Held at warn so adoption does not turn a green module red, and raised to error once the remaining findings are fixed.",
1691
+ options: ["in-try-catch"]
1647
1692
  },
1648
1693
  "typescript/switch-exhaustiveness-check": [
1649
1694
  "error",
@@ -2034,8 +2079,8 @@ var react_lib_default = {
2034
2079
  },
2035
2080
  "typescript/return-await": {
2036
2081
  severity: "warn",
2037
- reason: "replaces the deprecated core `no-return-await`. The `never` option is what makes it mean the same thing. Held at warn to match the app tier, where running it over every file found real violations the old rule never reported.",
2038
- options: ["never"]
2082
+ reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. `in-try-catch` rather than `never`, and the planning adoption is what settled it. `never` forces `return await f()` to become `const x = await f(); return x` even inside a `try`, where the await is REQUIRED for the catch to fire: it produced 10 such edits across 15 modules, every one of them inside a try/catch, and every one of them a rewrite that made the code longer without making it safer. Worse, the obvious reading of the rule is to delete the `await`, which silently stops the catch from running. `in-try-catch` is what typescript-eslint recommends and flags none of those 10. Held at warn so adoption does not turn a green module red, and raised to error once the remaining findings are fixed.",
2083
+ options: ["in-try-catch"]
2039
2084
  },
2040
2085
  "use-isnan": [
2041
2086
  "error",
@@ -2608,8 +2653,8 @@ var react_default = {
2608
2653
  "typescript/restrict-template-expressions": "error",
2609
2654
  "typescript/return-await": {
2610
2655
  severity: "warn",
2611
- reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. The `never` option matches what no-return-await meant, so this is like-for-like, but it now runs over every file and finds real violations (9 on bff-admin, 5 on host-admin, measured with --type-aware). Held at warn so adoption does not turn a green module red, and raised to error once they are fixed. Switching to the stricter `in-try-catch`, which typescript-eslint recommends because `return await` inside try IS needed for correct error handling, is a separate decision with its own count.",
2612
- options: ["never"]
2656
+ reason: "replaces the deprecated core `no-return-await`, which both modules set to error but which was never actually reporting: the nx lint target expanded to 2 files of 2888. `in-try-catch` rather than `never`, and the planning adoption is what settled it. `never` forces `return await f()` to become `const x = await f(); return x` even inside a `try`, where the await is REQUIRED for the catch to fire: it produced 10 such edits across 15 modules, every one of them inside a try/catch, and every one of them a rewrite that made the code longer without making it safer. Worse, the obvious reading of the rule is to delete the `await`, which silently stops the catch from running. `in-try-catch` is what typescript-eslint recommends and flags none of those 10. Held at warn so adoption does not turn a green module red, and raised to error once the remaining findings are fixed.",
2657
+ options: ["in-try-catch"]
2613
2658
  },
2614
2659
  "typescript/switch-exhaustiveness-check": {
2615
2660
  severity: "warn",
@@ -3070,6 +3115,10 @@ function downgradedRulesFor(preset) {
3070
3115
  return downgradedFor(preset);
3071
3116
  }
3072
3117
 
3118
+ // src/roles/lint/extra-layers.ts
3119
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3120
+ import { isAbsolute as isAbsolute2, join as join13, resolve as resolve3 } from "path";
3121
+
3073
3122
  // src/roles/lint/module-baseline.ts
3074
3123
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
3075
3124
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
@@ -3078,10 +3127,7 @@ function holdsFrom(errorCounts) {
3078
3127
  return [...errorCounts].map(([rule, count]) => ({ rule, count })).sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule));
3079
3128
  }
3080
3129
  function renderBaseline(holds, version) {
3081
- const entries = holds.map(
3082
- ({ rule, count }) => ` // ${count} violation${count === 1 ? "" : "s"} when this module adopted
3083
- ${JSON.stringify(rule)}: "warn"`
3084
- ).join(",\n");
3130
+ const entries = holds.map(({ rule }) => ` ${JSON.stringify(rule)}: "warn"`).join(",\n");
3085
3131
  return [
3086
3132
  "{",
3087
3133
  ` // Written by @hublo/sentinel@${version}. Do not edit by hand: \`sentinel --init --lint\``,
@@ -3089,9 +3135,15 @@ function renderBaseline(holds, version) {
3089
3135
  " // and returns to `error` on its own.",
3090
3136
  " //",
3091
3137
  " // These rules are held at `warn` because this module ALREADY violated them when it",
3092
- " // adopted. They still run and still report; they just cannot fail the build for code",
3093
- " // that was there before the migration. Fix them and re-run `--init --lint` to get the",
3094
- " // preset severity back.",
3138
+ " // adopted. They still run and still report, so the build cannot fail on them.",
3139
+ " //",
3140
+ " // The hold is per RULE, not per occurrence: oxlint cannot freeze a specific list of",
3141
+ " // violations, so a NEW violation of one of these rules is covered too, and will only",
3142
+ " // warn. That is the cost of adopting without a red build, and the reason to clear this",
3143
+ " // file rather than live with it.",
3144
+ " //",
3145
+ " // `sentinel --inspect --lint` counts what is left. Fix them and re-run `--init --lint`",
3146
+ " // to get the preset severity back.",
3095
3147
  ' "rules": {',
3096
3148
  entries,
3097
3149
  " }",
@@ -3111,6 +3163,44 @@ function describeHolds(holds) {
3111
3163
  return `held ${holds.length} rule(s) at \`warn\` for this module, covering ${total} violation(s) that were already there: ${listed}${rest}. They are written to ${LINT_BASELINE_FILE}, they still report, and they return to \`error\` once fixed and re-initialized`;
3112
3164
  }
3113
3165
 
3166
+ // src/roles/lint/extra-layers.ts
3167
+ function isSentinelPreset2(entry) {
3168
+ return entry.includes("@hublo/sentinel/oxlint/");
3169
+ }
3170
+ function ruleCount(cwd, specifier) {
3171
+ const path = isAbsolute2(specifier) ? specifier : resolve3(cwd, specifier);
3172
+ if (!existsSync11(path)) return void 0;
3173
+ try {
3174
+ const parsed = parseJsonc(
3175
+ readFileSync10(path, "utf8"),
3176
+ specifier
3177
+ );
3178
+ return Object.keys(parsed.rules ?? {}).length;
3179
+ } catch {
3180
+ return void 0;
3181
+ }
3182
+ }
3183
+ function extraLayers(cwd, extendsList) {
3184
+ return extendsList.filter((entry) => !isSentinelPreset2(entry) && entry !== LINT_BASELINE_SPECIFIER).map((entry) => {
3185
+ const count = ruleCount(cwd, entry);
3186
+ return {
3187
+ rule: entry,
3188
+ reason: count === void 0 ? "this module extends it, but it could not be read from here" : `${count} rule(s) on top of the preset, applied after it`
3189
+ };
3190
+ });
3191
+ }
3192
+ function committedExtendsList(cwd, configFile) {
3193
+ try {
3194
+ const parsed = parseJsonc(
3195
+ readFileSync10(join13(cwd, configFile), "utf8"),
3196
+ configFile
3197
+ );
3198
+ return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
3199
+ } catch {
3200
+ return [];
3201
+ }
3202
+ }
3203
+
3114
3204
  // src/roles/lint/parse-diagnostics.ts
3115
3205
  function ruleId(code) {
3116
3206
  const inner = /\(([^)]+)\)/.exec(code);
@@ -3161,8 +3251,8 @@ function errorCountsByConfigRule(stdout) {
3161
3251
  }
3162
3252
 
3163
3253
  // src/core/config/read-adoption.ts
3164
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3165
- import { join as join14 } from "path";
3254
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
3255
+ import { join as join15 } from "path";
3166
3256
 
3167
3257
  // src/core/config/owned-keys.ts
3168
3258
  function presetOwnedKeys(config, permitted, presetSets) {
@@ -3177,12 +3267,12 @@ function localOnlyKeys(config, permitted, presetSets) {
3177
3267
  }
3178
3268
 
3179
3269
  // src/core/config/resolve-config-target.ts
3180
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
3181
- import { join as join13 } from "path";
3270
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3271
+ import { join as join14 } from "path";
3182
3272
  function readExtends(absolutePath) {
3183
3273
  let parsed;
3184
3274
  try {
3185
- parsed = parseJsonc(readFileSync10(absolutePath, "utf8"), absolutePath);
3275
+ parsed = parseJsonc(readFileSync11(absolutePath, "utf8"), absolutePath);
3186
3276
  } catch {
3187
3277
  return [];
3188
3278
  }
@@ -3196,8 +3286,8 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
3196
3286
  let existing;
3197
3287
  let existingExtendsSomething = false;
3198
3288
  for (const candidate of candidates) {
3199
- const absolutePath = join13(moduleDir, candidate);
3200
- if (!existsSync11(absolutePath)) continue;
3289
+ const absolutePath = join14(moduleDir, candidate);
3290
+ if (!existsSync12(absolutePath)) continue;
3201
3291
  const chain = readExtends(absolutePath);
3202
3292
  if (existing === void 0) {
3203
3293
  existing = candidate;
@@ -3230,12 +3320,12 @@ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
3230
3320
  });
3231
3321
  function readAdoption(cwd, options) {
3232
3322
  const target = resolveConfigTarget(cwd, options);
3233
- if (target.reason === "none" || !existsSync12(join14(cwd, target.path))) {
3323
+ if (target.reason === "none" || !existsSync13(join15(cwd, target.path))) {
3234
3324
  return NOT_ADOPTED2(target.reason === "none" ? null : target.path);
3235
3325
  }
3236
3326
  let parsed;
3237
3327
  try {
3238
- parsed = parseJsonc(readFileSync11(join14(cwd, target.path), "utf8"), target.path);
3328
+ parsed = parseJsonc(readFileSync12(join15(cwd, target.path), "utf8"), target.path);
3239
3329
  } catch (error) {
3240
3330
  const reason = error instanceof Error ? error.message : String(error);
3241
3331
  return NOT_ADOPTED2(target.path, `${target.path} could not be parsed (${reason})`);
@@ -3269,9 +3359,9 @@ function readLintAdoption(cwd) {
3269
3359
  }
3270
3360
 
3271
3361
  // src/roles/lint/resolve-oxlint.ts
3272
- import { existsSync as existsSync13 } from "fs";
3362
+ import { existsSync as existsSync14 } from "fs";
3273
3363
  import { createRequire as createRequire3 } from "module";
3274
- import { delimiter as delimiter2, dirname as dirname5, join as join15 } from "path";
3364
+ import { delimiter as delimiter2, dirname as dirname5, join as join16 } from "path";
3275
3365
  import { fileURLToPath as fileURLToPath2 } from "url";
3276
3366
  var PACKAGE_OF = {
3277
3367
  oxlint: "oxlint",
@@ -3296,30 +3386,30 @@ function tsgolintShim(cwd) {
3296
3386
  for (const owner of ["oxlint-tsgolint", "oxlint"]) {
3297
3387
  try {
3298
3388
  const packageDir = dirname5(require3.resolve(`${owner}/package.json`));
3299
- candidates.push(join15(packageDir, "node_modules", ".bin", "tsgolint"));
3300
- candidates.push(join15(packageDir, "..", ".bin", "tsgolint"));
3389
+ candidates.push(join16(packageDir, "node_modules", ".bin", "tsgolint"));
3390
+ candidates.push(join16(packageDir, "..", ".bin", "tsgolint"));
3301
3391
  } catch {
3302
3392
  }
3303
3393
  }
3304
3394
  try {
3305
3395
  const ownRoot = dirname5(require3.resolve("@hublo/sentinel/package.json"));
3306
- candidates.push(join15(ownRoot, "node_modules", ".bin", "tsgolint"));
3396
+ candidates.push(join16(ownRoot, "node_modules", ".bin", "tsgolint"));
3307
3397
  } catch {
3308
3398
  candidates.push(resolveBin(dirname5(fileURLToPath2(import.meta.url)), "tsgolint") ?? "");
3309
3399
  }
3310
- return candidates.find((candidate) => candidate !== "" && existsSync13(candidate));
3400
+ return candidates.find((candidate) => candidate !== "" && existsSync14(candidate));
3311
3401
  }
3312
3402
  function oxlintSearchPath(cwd) {
3313
3403
  return binSearchPath(cwd);
3314
3404
  }
3315
3405
 
3316
3406
  // src/roles/lint/adapters/oxlint/plan.ts
3317
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
3318
- import { join as join18 } from "path";
3407
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
3408
+ import { join as join19 } from "path";
3319
3409
 
3320
3410
  // src/roles/lint/eslint-ignores.ts
3321
- import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
3322
- import { join as join16 } from "path";
3411
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3412
+ import { join as join17 } from "path";
3323
3413
  var IGNORE_BLOCKS = [/\bignores\s*:\s*\[([^\]]*)\]/g, /\bglobalIgnores\s*\(\s*\[([^\]]*)\]/g];
3324
3414
  var STRING_LITERAL = /['"`]([^'"`]+)['"`]/g;
3325
3415
  var OPAQUE_SOURCE = /\b(includeIgnoreFile)\s*\([^)]*\)/g;
@@ -3332,11 +3422,11 @@ function readRootEslintIgnores(root) {
3332
3422
  return { patterns: patterns.filter((pattern) => pattern.startsWith("**/")), unresolved };
3333
3423
  }
3334
3424
  function readEslintIgnores(cwd) {
3335
- const config = ESLINT_CONFIG_FILES.map((name) => join16(cwd, name)).find((path) => existsSync14(path));
3425
+ const config = ESLINT_CONFIG_FILES.map((name) => join17(cwd, name)).find((path) => existsSync15(path));
3336
3426
  if (!config) return { patterns: [], unresolved: [] };
3337
3427
  let source;
3338
3428
  try {
3339
- source = readFileSync12(config, "utf8");
3429
+ source = readFileSync13(config, "utf8");
3340
3430
  } catch {
3341
3431
  return { patterns: [], unresolved: [] };
3342
3432
  }
@@ -3390,8 +3480,8 @@ function lintPresetFor(preset) {
3390
3480
  }
3391
3481
 
3392
3482
  // src/roles/lint/rename-suppressions.ts
3393
- import { readdirSync as readdirSync2, readFileSync as readFileSync13, statSync } from "fs";
3394
- import { join as join17, relative as relative2 } from "path";
3483
+ import { readdirSync as readdirSync2, readFileSync as readFileSync14, statSync } from "fs";
3484
+ import { join as join18, relative as relative2 } from "path";
3395
3485
  var SOURCE_EXTENSIONS = [
3396
3486
  ".ts",
3397
3487
  ".tsx",
@@ -3438,7 +3528,7 @@ function* sourceFiles(dir) {
3438
3528
  return;
3439
3529
  }
3440
3530
  for (const entry of entries) {
3441
- const full = join17(dir, entry);
3531
+ const full = join18(dir, entry);
3442
3532
  let isDirectory;
3443
3533
  try {
3444
3534
  isDirectory = statSync(full).isDirectory();
@@ -3458,7 +3548,7 @@ function findSuppressionRenames(cwd, renames) {
3458
3548
  for (const file of sourceFiles(cwd)) {
3459
3549
  let content;
3460
3550
  try {
3461
- content = readFileSync13(file, "utf8");
3551
+ content = readFileSync14(file, "utf8");
3462
3552
  } catch {
3463
3553
  continue;
3464
3554
  }
@@ -3498,7 +3588,17 @@ var DEFAULT_IGNORE_PATTERNS = [
3498
3588
  "**/dist/**",
3499
3589
  "**/node_modules/**",
3500
3590
  "**/coverage/**",
3501
- "**/*.mock.ts",
3591
+ // `**/*.mock.ts` used to sit here and has been removed. It was the only entry in this list
3592
+ // with no reason written next to it, and it does not meet the bar the paragraph above sets:
3593
+ // a mock is not build output, not vendored and not generated. It is hand-written TypeScript
3594
+ // that ships with the tests, and lint rules have as much to say about it as about any other
3595
+ // source file. Ignoring it quietly narrowed what an adopted module checks — career lost
3596
+ // `src/test/router.mock.ts`, which its previous ESLint config did lint.
3597
+ //
3598
+ // Measured before removing, because this is the one change that can turn a green module red:
3599
+ // career has 1 such file and it reports nothing; the 15 planning modules have none. 222 exist
3600
+ // repo-wide, and modules adopting later meet them through the baseline, which is exactly what
3601
+ // the baseline is for.
3502
3602
  // Tool configuration is not source. A repo's shared ESLint config ignores these, and every
3503
3603
  // module inherits that; adoption replaces the config chain, so without them the first thing
3504
3604
  // an adopted module lints is its own jest setup file. Found on libs/front/components, where
@@ -3561,7 +3661,7 @@ function plan(context) {
3561
3661
  ].filter((pattern) => !defaults.has(bare(pattern)));
3562
3662
  const carried = [.../* @__PURE__ */ new Set([...DEFAULT_IGNORE_PATTERNS, ...moduleOwn])];
3563
3663
  const stub = {
3564
- extends: [presetPath(variant)],
3664
+ extends: extendsWithPreset(committedExtends(context.cwd), variant),
3565
3665
  ...carried.length > 0 ? { ignorePatterns: carried } : {}
3566
3666
  };
3567
3667
  const operations = [
@@ -3576,7 +3676,7 @@ function plan(context) {
3576
3676
  keys: removableDeps
3577
3677
  });
3578
3678
  }
3579
- const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync15(join18(context.cwd, name)));
3679
+ const eslintConfigs = ESLINT_CONFIG_FILES.filter((name) => existsSync16(join19(context.cwd, name)));
3580
3680
  for (const name of eslintConfigs) operations.push({ kind: "delete", path: name });
3581
3681
  operations.push(
3582
3682
  ...nxTargetOperations({
@@ -3671,10 +3771,21 @@ function lintScripts(cwd) {
3671
3771
  }
3672
3772
  return rewritten;
3673
3773
  }
3774
+ function committedExtends(cwd) {
3775
+ try {
3776
+ const parsed = parseJsonc(
3777
+ readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
3778
+ LINT_CONFIG_FILE
3779
+ );
3780
+ return Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : [];
3781
+ } catch {
3782
+ return [];
3783
+ }
3784
+ }
3674
3785
  function committedIgnorePatterns(cwd) {
3675
3786
  try {
3676
3787
  const parsed = parseJsonc(
3677
- readFileSync14(join18(cwd, LINT_CONFIG_FILE), "utf8"),
3788
+ readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
3678
3789
  LINT_CONFIG_FILE
3679
3790
  );
3680
3791
  return Array.isArray(parsed.ignorePatterns) ? parsed.ignorePatterns.filter((entry) => typeof entry === "string") : [];
@@ -3686,7 +3797,7 @@ function moduleEslintDependencies(cwd) {
3686
3797
  const isEslintPackage = (name) => name === "eslint" || name === "@types/eslint" || name === "typescript-eslint" || name.startsWith("@typescript-eslint/") || name.startsWith("eslint-plugin-") || name.startsWith("eslint-config-") || name.startsWith("@eslint/");
3687
3798
  let manifest;
3688
3799
  try {
3689
- manifest = JSON.parse(readFileSync14(join18(cwd, "package.json"), "utf8"));
3800
+ manifest = JSON.parse(readFileSync15(join19(cwd, "package.json"), "utf8"));
3690
3801
  } catch {
3691
3802
  return [];
3692
3803
  }
@@ -3805,20 +3916,20 @@ var OxlintAdapter = class extends BaseAdapter {
3805
3916
  * build the developer can see, rather than a silent half-adoption they cannot.
3806
3917
  */
3807
3918
  writeModuleBaseline(ctx, oxlint, env) {
3808
- const configPath = join19(ctx.cwd, LINT_CONFIG_FILE);
3809
- const baselinePath = join19(ctx.cwd, LINT_BASELINE_FILE);
3810
- if (!existsSync16(configPath)) return;
3919
+ const configPath = join20(ctx.cwd, LINT_CONFIG_FILE);
3920
+ const baselinePath = join20(ctx.cwd, LINT_BASELINE_FILE);
3921
+ if (!existsSync17(configPath)) return;
3811
3922
  let config;
3812
3923
  try {
3813
3924
  config = parseJsonc(
3814
- readFileSync15(configPath, "utf8"),
3925
+ readFileSync16(configPath, "utf8"),
3815
3926
  LINT_CONFIG_FILE
3816
3927
  );
3817
3928
  } catch {
3818
3929
  return;
3819
3930
  }
3820
3931
  const current = Array.isArray(config.extends) ? config.extends : [];
3821
- const measurePath = join19(ctx.cwd, LINT_MEASURE_FILE);
3932
+ const measurePath = join20(ctx.cwd, LINT_MEASURE_FILE);
3822
3933
  let measured;
3823
3934
  try {
3824
3935
  writeFileSync2(
@@ -3866,7 +3977,7 @@ var OxlintAdapter = class extends BaseAdapter {
3866
3977
  }
3867
3978
  }
3868
3979
  async run(ctx) {
3869
- if (!existsSync16(join19(ctx.cwd, LINT_CONFIG_FILE))) {
3980
+ if (!existsSync17(join20(ctx.cwd, LINT_CONFIG_FILE))) {
3870
3981
  process.stderr.write(
3871
3982
  `sentinel lint(oxlint): no ${LINT_CONFIG_FILE} in this module; run \`sentinel --init --lint\` to adopt.
3872
3983
  `
@@ -3950,7 +4061,12 @@ var OxlintAdapter = class extends BaseAdapter {
3950
4061
  downgraded: downgradedRulesFor(presetVariant(ctx.preset, ctx.cwd)).map((entry) => ({
3951
4062
  rule: entry.rule,
3952
4063
  reason: entry.reason
3953
- }))
4064
+ })),
4065
+ // What this module enforces BEYOND the preset. Not drift, and not covered by anything
4066
+ // else here: the stub still holds only `extends` and `ignorePatterns`, so a module with
4067
+ // a team layer reports `conformant=true drift=[]` and used to say nothing at all about
4068
+ // the extra rules it runs. See `extra-layers` for why that state is legitimate.
4069
+ layers: extraLayers(ctx.cwd, committedExtendsList(ctx.cwd, LINT_CONFIG_FILE))
3954
4070
  };
3955
4071
  }
3956
4072
  /** How many rules the extended preset enforces, read from the preset on disk. */
@@ -3981,7 +4097,7 @@ var OxlintAdapter = class extends BaseAdapter {
3981
4097
  let stub;
3982
4098
  try {
3983
4099
  stub = parseJsonc(
3984
- readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
4100
+ readFileSync16(join20(cwd, LINT_CONFIG_FILE), "utf8"),
3985
4101
  LINT_CONFIG_FILE
3986
4102
  );
3987
4103
  } catch {
@@ -3991,7 +4107,7 @@ var OxlintAdapter = class extends BaseAdapter {
3991
4107
  for (const entry of stub.extends ?? []) {
3992
4108
  try {
3993
4109
  const preset = parseJsonc(
3994
- readFileSync15(join19(cwd, entry), "utf8"),
4110
+ readFileSync16(join20(cwd, entry), "utf8"),
3995
4111
  entry
3996
4112
  );
3997
4113
  for (const rule of Object.keys(preset.rules ?? {})) names.add(rule);
@@ -4061,14 +4177,14 @@ var OxlintAdapter = class extends BaseAdapter {
4061
4177
  let parsed;
4062
4178
  try {
4063
4179
  parsed = parseJsonc(
4064
- readFileSync15(join19(cwd, LINT_CONFIG_FILE), "utf8"),
4180
+ readFileSync16(join20(cwd, LINT_CONFIG_FILE), "utf8"),
4065
4181
  LINT_CONFIG_FILE
4066
4182
  );
4067
4183
  } catch {
4068
4184
  return void 0;
4069
4185
  }
4070
4186
  const targets = Array.isArray(parsed.extends) ? parsed.extends.filter((entry) => typeof entry === "string") : typeof parsed.extends === "string" ? [parsed.extends] : [];
4071
- return targets.find((target) => !existsSync16(join19(cwd, target)));
4187
+ return targets.find((target) => !existsSync17(join20(cwd, target)));
4072
4188
  }
4073
4189
  /** Announce what is not enforced, so reduced coverage is never silent. */
4074
4190
  announceDisabled(preset) {
@@ -4100,9 +4216,9 @@ function registerLint() {
4100
4216
 
4101
4217
  // src/roles/typescript/adapters/tsc/tsc.adapter.ts
4102
4218
  import { spawnSync as spawnSync3 } from "child_process";
4103
- import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
4219
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
4104
4220
  import { createRequire as createRequire4 } from "module";
4105
- import { join as join21 } from "path";
4221
+ import { join as join22 } from "path";
4106
4222
 
4107
4223
  // src/roles/typescript/presets/base.json
4108
4224
  var base_default3 = {
@@ -4283,8 +4399,8 @@ function readTsconfigAdoption(cwd) {
4283
4399
  }
4284
4400
 
4285
4401
  // src/roles/typescript/adapters/tsc/plan.ts
4286
- import { readFileSync as readFileSync16 } from "fs";
4287
- import { join as join20 } from "path";
4402
+ import { readFileSync as readFileSync17 } from "fs";
4403
+ import { join as join21 } from "path";
4288
4404
 
4289
4405
  // src/roles/typescript/typecheck-script.ts
4290
4406
  var SENTINEL_TYPECHECK_COMMAND = "sentinel --run --typescript";
@@ -4306,7 +4422,10 @@ function keepsForeignChecker(existing) {
4306
4422
 
4307
4423
  // src/roles/typescript/adapters/tsc/plan.ts
4308
4424
  var INSTALL_NOTE = "run `pnpm install` to fetch @hublo/sentinel (added to the module devDependencies) so `extends` and the typecheck script resolve";
4309
- var TYPECHECK_TARGET = { cache: true, inputs: ["default", "{projectRoot}/tsconfig.json"] };
4425
+ var TYPECHECK_TARGET = {
4426
+ cache: true,
4427
+ inputs: ["default", "^default", "{projectRoot}/tsconfig.json"]
4428
+ };
4310
4429
  function typecheckScripts(cwd) {
4311
4430
  return {
4312
4431
  // Reads the nx target too, not only the npm script: five modules in this repo keep a
@@ -4372,7 +4491,7 @@ function planAdoption(context) {
4372
4491
  };
4373
4492
  }
4374
4493
  const existing = parseJsonc(
4375
- readFileSync16(join20(context.cwd, target.path), "utf8"),
4494
+ readFileSync17(join21(context.cwd, target.path), "utf8"),
4376
4495
  target.path
4377
4496
  );
4378
4497
  const extendsChain = composeExtends(existing.extends, preset);
@@ -4510,7 +4629,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4510
4629
  let chain;
4511
4630
  try {
4512
4631
  const parsed = parseJsonc(
4513
- readFileSync17(join21(cwd, target.path), "utf8"),
4632
+ readFileSync18(join22(cwd, target.path), "utf8"),
4514
4633
  target.path
4515
4634
  );
4516
4635
  chain = parsed.extends;
@@ -4523,7 +4642,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4523
4642
  );
4524
4643
  if (preset === void 0) return void 0;
4525
4644
  try {
4526
- createRequire4(join21(cwd, "noop.js")).resolve(preset);
4645
+ createRequire4(join22(cwd, "noop.js")).resolve(preset);
4527
4646
  return void 0;
4528
4647
  } catch {
4529
4648
  return preset;
@@ -4619,7 +4738,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4619
4738
  referencedProjects(cwd, config) {
4620
4739
  try {
4621
4740
  const parsed = parseJsonc(
4622
- readFileSync17(join21(cwd, config), "utf8"),
4741
+ readFileSync18(join22(cwd, config), "utf8"),
4623
4742
  config
4624
4743
  );
4625
4744
  const referenced = (parsed.references ?? []).map((reference) => reference.path).filter((path) => typeof path === "string" && path.length > 0);
@@ -4635,7 +4754,7 @@ var TscAdapter = class _TscAdapter extends BaseAdapter {
4635
4754
  * check.
4636
4755
  */
4637
4756
  typecheckTarget(cwd) {
4638
- if (existsSync17(join21(cwd, "tsconfig.json"))) return "tsconfig.json";
4757
+ if (existsSync18(join22(cwd, "tsconfig.json"))) return "tsconfig.json";
4639
4758
  const target = resolveTsconfigTarget(cwd);
4640
4759
  return target.reason === "none" ? null : target.path;
4641
4760
  }
@@ -4838,8 +4957,8 @@ function replaceLines(current, replacements) {
4838
4957
  }
4839
4958
 
4840
4959
  // src/core/apply-plan.ts
4841
- import { existsSync as existsSync18, readFileSync as readFileSync18, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
4842
- import { resolve as resolve3, sep } from "path";
4960
+ import { existsSync as existsSync19, readFileSync as readFileSync19, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
4961
+ import { resolve as resolve4, sep } from "path";
4843
4962
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
4844
4963
 
4845
4964
  // src/shared/deep-merge.ts
@@ -4849,15 +4968,15 @@ function isPlainObject(value) {
4849
4968
 
4850
4969
  // src/core/apply-plan.ts
4851
4970
  function resolveWithinRoot(cwd, relativePath) {
4852
- const root = resolve3(cwd);
4853
- const absolutePath = resolve3(root, relativePath);
4971
+ const root = resolve4(cwd);
4972
+ const absolutePath = resolve4(root, relativePath);
4854
4973
  if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
4855
4974
  throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
4856
4975
  }
4857
4976
  return absolutePath;
4858
4977
  }
4859
4978
  function readIfExists(absolutePath) {
4860
- return existsSync18(absolutePath) ? readFileSync18(absolutePath, "utf8") : void 0;
4979
+ return existsSync19(absolutePath) ? readFileSync19(absolutePath, "utf8") : void 0;
4861
4980
  }
4862
4981
  function* leaves(value, prefix = []) {
4863
4982
  for (const [key, keyValue] of Object.entries(value)) {
@@ -4975,8 +5094,8 @@ function applyPlan(cwd, plan2) {
4975
5094
  }
4976
5095
 
4977
5096
  // src/core/config/preset-evidence.ts
4978
- import { existsSync as existsSync19, readdirSync as readdirSync3, readFileSync as readFileSync19 } from "fs";
4979
- import { join as join22 } from "path";
5097
+ import { existsSync as existsSync20, readdirSync as readdirSync3, readFileSync as readFileSync20 } from "fs";
5098
+ import { join as join23 } from "path";
4980
5099
  var PATH_SIGNALS = [
4981
5100
  {
4982
5101
  preset: "nest",
@@ -4990,11 +5109,11 @@ var DEPENDENCY_SIGNALS = [
4990
5109
  { preset: "nest", pattern: /^@nestjs\// }
4991
5110
  ];
4992
5111
  function dependencyNames(cwd) {
4993
- const path = join22(cwd, "package.json");
4994
- if (!existsSync19(path)) return [];
5112
+ const path = join23(cwd, "package.json");
5113
+ if (!existsSync20(path)) return [];
4995
5114
  try {
4996
5115
  const manifest = parseJsonc(
4997
- readFileSync19(path, "utf8"),
5116
+ readFileSync20(path, "utf8"),
4998
5117
  path
4999
5118
  );
5000
5119
  return [
@@ -5017,7 +5136,7 @@ function declaresJsx(cwd) {
5017
5136
  for (const name of entries) {
5018
5137
  try {
5019
5138
  const config = parseJsonc(
5020
- readFileSync19(join22(cwd, name), "utf8"),
5139
+ readFileSync20(join23(cwd, name), "utf8"),
5021
5140
  name
5022
5141
  );
5023
5142
  if (config.compilerOptions?.jsx !== void 0) return true;
@@ -5167,6 +5286,7 @@ export {
5167
5286
  WORKSPACE_ROOT_MARKER,
5168
5287
  findWorkspaceRoot,
5169
5288
  ensureWorkspacePrep,
5289
+ inspectWorkspacePrep,
5170
5290
  palette,
5171
5291
  resolveBin,
5172
5292
  VERBS,
@@ -5177,4 +5297,4 @@ export {
5177
5297
  detectFramework,
5178
5298
  dispatch
5179
5299
  };
5180
- //# sourceMappingURL=chunk-RLVLDC5H.js.map
5300
+ //# sourceMappingURL=chunk-RGIEWANO.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-RLVLDC5H.js";
10
+ } from "./chunk-RGIEWANO.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
package/oxlint/nest.json CHANGED
@@ -327,7 +327,7 @@
327
327
  ],
328
328
  "typescript/return-await": [
329
329
  "warn",
330
- "never"
330
+ "in-try-catch"
331
331
  ],
332
332
  "typescript/switch-exhaustiveness-check": [
333
333
  "error",
@@ -319,7 +319,7 @@
319
319
  "typescript/no-useless-constructor": "warn",
320
320
  "typescript/return-await": [
321
321
  "warn",
322
- "never"
322
+ "in-try-catch"
323
323
  ],
324
324
  "use-isnan": [
325
325
  "error",
package/oxlint/react.json CHANGED
@@ -438,7 +438,7 @@
438
438
  "typescript/restrict-template-expressions": "error",
439
439
  "typescript/return-await": [
440
440
  "warn",
441
- "never"
441
+ "in-try-catch"
442
442
  ],
443
443
  "typescript/switch-exhaustiveness-check": "warn",
444
444
  "typescript/unbound-method": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",