@hublo/sentinel 1.1.0 → 1.1.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.
@@ -16,7 +16,7 @@ import {
16
16
  registerAdapters,
17
17
  resolve,
18
18
  resolveBin
19
- } from "../chunk-FA6N4XCP.js";
19
+ } from "../chunk-NHRBD2UJ.js";
20
20
 
21
21
  // bin/sentinel.ts
22
22
  import { program } from "commander";
@@ -1183,7 +1183,7 @@ function registerFormat() {
1183
1183
 
1184
1184
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
1185
1185
  import { spawnSync as spawnSync2 } from "child_process";
1186
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
1186
+ import { existsSync as existsSync16, readFileSync as readFileSync14, rmSync, writeFileSync as writeFileSync2 } from "fs";
1187
1187
  import { join as join18 } from "path";
1188
1188
 
1189
1189
  // src/core/config/deferred-rules.ts
@@ -2989,22 +2989,72 @@ function downgradedRulesFor(preset) {
2989
2989
  return downgradedFor(preset);
2990
2990
  }
2991
2991
 
2992
+ // src/roles/lint/module-baseline.ts
2993
+ var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
2994
+ var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
2995
+ var LINT_BASELINE_SPECIFIER = `./${LINT_BASELINE_FILE}`;
2996
+ function holdsFrom(errorCounts) {
2997
+ return [...errorCounts].map(([rule, count]) => ({ rule, count })).sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule));
2998
+ }
2999
+ function renderBaseline(holds, version) {
3000
+ const entries = holds.map(
3001
+ ({ rule, count }) => ` // ${count} violation${count === 1 ? "" : "s"} when this module adopted
3002
+ ${JSON.stringify(rule)}: "warn"`
3003
+ ).join(",\n");
3004
+ return [
3005
+ "{",
3006
+ ` // Written by @hublo/sentinel@${version}. Do not edit by hand: \`sentinel --init --lint\``,
3007
+ " // regenerates it from a fresh measurement, and a rule you have since fixed drops out",
3008
+ " // and returns to `error` on its own.",
3009
+ " //",
3010
+ " // These rules are held at `warn` because this module ALREADY violated them when it",
3011
+ " // adopted. They still run and still report; they just cannot fail the build for code",
3012
+ " // that was there before the migration. Fix them and re-run `--init --lint` to get the",
3013
+ " // preset severity back.",
3014
+ ' "rules": {',
3015
+ entries,
3016
+ " }",
3017
+ "}",
3018
+ ""
3019
+ ].join("\n");
3020
+ }
3021
+ function extendsWithBaseline(current, hasHolds) {
3022
+ const withoutBaseline = current.filter((entry) => entry !== LINT_BASELINE_SPECIFIER);
3023
+ return hasHolds ? [...withoutBaseline, LINT_BASELINE_SPECIFIER] : withoutBaseline;
3024
+ }
3025
+ function describeHolds(holds) {
3026
+ if (holds.length === 0) return "";
3027
+ const total = holds.reduce((sum, hold) => sum + hold.count, 0);
3028
+ const listed = holds.slice(0, 5).map(({ rule, count }) => `${rule} (${count})`).join(", ");
3029
+ const rest = holds.length > 5 ? `, and ${holds.length - 5} more` : "";
3030
+ 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`;
3031
+ }
3032
+
2992
3033
  // src/roles/lint/parse-diagnostics.ts
2993
3034
  function ruleId(code) {
2994
3035
  const inner = /\(([^)]+)\)/.exec(code);
2995
3036
  return inner?.[1] ?? code;
2996
3037
  }
2997
- function parseOxlintDiagnostics(stdout) {
3038
+ function configRuleId(code) {
3039
+ const match = /^([^(]+)\(([^)]+)\)$/.exec(code.trim());
3040
+ if (!match) return code.trim();
3041
+ const plugin = match[1] ?? "";
3042
+ const rule = match[2] ?? "";
3043
+ return plugin === "eslint" ? rule : `${plugin}/${rule}`;
3044
+ }
3045
+ function rawDiagnostics(stdout) {
2998
3046
  const start = stdout.indexOf("{");
2999
3047
  if (start === -1) return [];
3000
- let parsed;
3001
3048
  try {
3002
- parsed = JSON.parse(stdout.slice(start));
3049
+ const parsed = JSON.parse(stdout.slice(start));
3050
+ return parsed.diagnostics ?? [];
3003
3051
  } catch {
3004
3052
  return [];
3005
3053
  }
3054
+ }
3055
+ function parseOxlintDiagnostics(stdout) {
3006
3056
  const out = [];
3007
- for (const entry of parsed.diagnostics ?? []) {
3057
+ for (const entry of rawDiagnostics(stdout)) {
3008
3058
  const span = entry.labels?.[0]?.span;
3009
3059
  out.push({
3010
3060
  file: typeof entry.filename === "string" ? entry.filename : "",
@@ -3017,6 +3067,17 @@ function parseOxlintDiagnostics(stdout) {
3017
3067
  }
3018
3068
  return out;
3019
3069
  }
3070
+ function errorCountsByConfigRule(stdout) {
3071
+ const counts = /* @__PURE__ */ new Map();
3072
+ for (const entry of rawDiagnostics(stdout)) {
3073
+ if (entry.severity !== "error") continue;
3074
+ const code = typeof entry.code === "string" ? entry.code : "";
3075
+ const id = configRuleId(code);
3076
+ if (!id) continue;
3077
+ counts.set(id, (counts.get(id) ?? 0) + 1);
3078
+ }
3079
+ return counts;
3080
+ }
3020
3081
 
3021
3082
  // src/core/config/read-adoption.ts
3022
3083
  import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
@@ -3549,6 +3610,7 @@ function moduleEslintDependencies(cwd) {
3549
3610
  }
3550
3611
 
3551
3612
  // src/roles/lint/adapters/oxlint/oxlint.adapter.ts
3613
+ var CAPTURE_MAX_BUFFER = 64 * 1024 * 1024;
3552
3614
  function summariseByPlugin2(rules) {
3553
3615
  const counts = /* @__PURE__ */ new Map();
3554
3616
  for (const name of deferredRuleNames(rules)) {
@@ -3612,7 +3674,8 @@ var OxlintAdapter = class extends BaseAdapter {
3612
3674
  const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, "--fix", "."], {
3613
3675
  cwd: ctx.cwd,
3614
3676
  encoding: "utf8",
3615
- env
3677
+ env,
3678
+ maxBuffer: CAPTURE_MAX_BUFFER
3616
3679
  });
3617
3680
  return result.status ?? 1;
3618
3681
  }
@@ -3638,6 +3701,85 @@ var OxlintAdapter = class extends BaseAdapter {
3638
3701
  if (!oxlint) return;
3639
3702
  const env = { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) };
3640
3703
  this.fixPass(ctx, oxlint, env);
3704
+ this.writeModuleBaseline(ctx, oxlint, env);
3705
+ }
3706
+ /**
3707
+ * Measure what THIS module already violates, and hold exactly those rules at `warn`.
3708
+ *
3709
+ * Runs after the autofix pass on purpose: a rule the fixer can clear is not debt, and
3710
+ * holding it back would freeze a severity over code that was about to be correct anyway.
3711
+ * What is left is the debt that needs a human, which is what the promise is about.
3712
+ *
3713
+ * WITH type information, unlike the fix pass. That costs time on a large module, and it is
3714
+ * not optional: the two rules that first broke this promise in the monorepo were
3715
+ * `jsx-a11y/no-noninteractive-tabindex` and `typescript/no-duplicate-type-constituents`,
3716
+ * and measuring without types would have missed the second and shipped the same bug with
3717
+ * more ceremony. Adoption happens once; a wrong promise is permanent.
3718
+ *
3719
+ * Never fails adoption. A module that adopted correctly must not be reported as broken
3720
+ * because a measurement could not run — the worst case is the previous behaviour, a red
3721
+ * build the developer can see, rather than a silent half-adoption they cannot.
3722
+ */
3723
+ writeModuleBaseline(ctx, oxlint, env) {
3724
+ const configPath = join18(ctx.cwd, LINT_CONFIG_FILE);
3725
+ const baselinePath = join18(ctx.cwd, LINT_BASELINE_FILE);
3726
+ if (!existsSync16(configPath)) return;
3727
+ let config;
3728
+ try {
3729
+ config = parseJsonc(
3730
+ readFileSync14(configPath, "utf8"),
3731
+ LINT_CONFIG_FILE
3732
+ );
3733
+ } catch {
3734
+ return;
3735
+ }
3736
+ const current = Array.isArray(config.extends) ? config.extends : [];
3737
+ const measurePath = join18(ctx.cwd, LINT_MEASURE_FILE);
3738
+ let measured;
3739
+ try {
3740
+ writeFileSync2(
3741
+ measurePath,
3742
+ `${JSON.stringify({ ...config, extends: extendsWithBaseline(current, false) }, null, 2)}
3743
+ `
3744
+ );
3745
+ measured = spawnSync2(
3746
+ oxlint,
3747
+ [
3748
+ "-c",
3749
+ LINT_MEASURE_FILE,
3750
+ ...canRunTypeAware(ctx.cwd) ? ["--type-aware"] : [],
3751
+ "-f",
3752
+ "json",
3753
+ "."
3754
+ ],
3755
+ { cwd: ctx.cwd, encoding: "utf8", env, maxBuffer: CAPTURE_MAX_BUFFER }
3756
+ );
3757
+ } finally {
3758
+ rmSync(measurePath, { force: true });
3759
+ }
3760
+ if (measured.error || !(measured.stdout ?? "").includes('"diagnostics"')) {
3761
+ process.stderr.write(
3762
+ ` ${palette(process.stderr).warn(
3763
+ `could not measure this module's existing violations, so ${LINT_BASELINE_FILE} was left as it was. Run \`sentinel --init --lint\` again after \`pnpm install\`.`
3764
+ )}
3765
+ `
3766
+ );
3767
+ return;
3768
+ }
3769
+ const holds = holdsFrom(errorCountsByConfigRule(measured.stdout ?? ""));
3770
+ const next = extendsWithBaseline(current, holds.length > 0);
3771
+ if (holds.length > 0) {
3772
+ writeFileSync2(baselinePath, renderBaseline(holds, readOwnPackage().version));
3773
+ process.stderr.write(` ${palette(process.stderr).warn(describeHolds(holds))}
3774
+ `);
3775
+ } else {
3776
+ rmSync(baselinePath, { force: true });
3777
+ }
3778
+ if (next.length !== current.length || next.some((entry, at) => entry !== current[at])) {
3779
+ config.extends = next;
3780
+ writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
3781
+ `);
3782
+ }
3641
3783
  }
3642
3784
  async run(ctx) {
3643
3785
  if (!existsSync16(join18(ctx.cwd, LINT_CONFIG_FILE))) {
@@ -3753,7 +3895,8 @@ var OxlintAdapter = class extends BaseAdapter {
3753
3895
  const result = spawnSync2(oxlint, ["-c", LINT_CONFIG_FILE, ...typeAware, "-f", "json", "."], {
3754
3896
  cwd: ctx.cwd,
3755
3897
  encoding: "utf8",
3756
- env: { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) }
3898
+ env: { ...process.env, PATH: oxlintPath(ctx.cwd, process.env) },
3899
+ maxBuffer: CAPTURE_MAX_BUFFER
3757
3900
  });
3758
3901
  if (result.error) {
3759
3902
  return { ok: false, code: 1, metrics: { error: result.error.message } };
@@ -4573,7 +4716,7 @@ function replaceLines(current, replacements) {
4573
4716
  }
4574
4717
 
4575
4718
  // src/core/apply-plan.ts
4576
- import { existsSync as existsSync18, readFileSync as readFileSync17, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
4719
+ import { existsSync as existsSync18, readFileSync as readFileSync17, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
4577
4720
  import { resolve as resolve3, sep } from "path";
4578
4721
  import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
4579
4722
 
@@ -4694,14 +4837,14 @@ function preparePlan(cwd, plan2) {
4694
4837
  }
4695
4838
  function writeFileAtomic(absolutePath, contents) {
4696
4839
  const tempPath = `${absolutePath}.sentinel-${process.pid}.tmp`;
4697
- writeFileSync2(tempPath, contents);
4840
+ writeFileSync3(tempPath, contents);
4698
4841
  renameSync(tempPath, absolutePath);
4699
4842
  }
4700
4843
  function applyPlan(cwd, plan2) {
4701
4844
  const changed = preparePlan(cwd, plan2).filter((file) => file.before !== file.after);
4702
4845
  for (const file of changed) {
4703
4846
  if (file.deleted) {
4704
- rmSync(file.absolutePath, { force: true });
4847
+ rmSync2(file.absolutePath, { force: true });
4705
4848
  continue;
4706
4849
  }
4707
4850
  writeFileAtomic(file.absolutePath, file.after);
@@ -4912,4 +5055,4 @@ export {
4912
5055
  detectFramework,
4913
5056
  dispatch
4914
5057
  };
4915
- //# sourceMappingURL=chunk-FA6N4XCP.js.map
5058
+ //# sourceMappingURL=chunk-NHRBD2UJ.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-FA6N4XCP.js";
10
+ } from "./chunk-NHRBD2UJ.js";
11
11
  export {
12
12
  BaseAdapter,
13
13
  all,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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",