@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.
package/dist/bin/sentinel.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
5058
|
+
//# sourceMappingURL=chunk-NHRBD2UJ.js.map
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.1.
|
|
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",
|