chore(lint): vendor anti-slop oxlint plugin and add batched cleanup pipeline

Vendor the anti-slop Oxlint plugin at tools/oxlint/anti-slop and register it
in oxlint.config.ts, with Oxlint's own rule categories disabled so ESLint
stays the general-purpose linter.

Add scripts/anti-slop.mjs (bun run deslop) mirroring the React Doctor batch
interface: next-batch, check-batch, active, release, top, file. Batch handoff
directories now double as file claims shared across clones via
~/.openchamber/maintenance-claims, so concurrent maintenance batches from
either pipeline never select the same file.

Harden both scheduled maintenance flows: stop on a dirty worktree, stop on
NO BATCH AVAILABLE, validate per package instead of workspace-wide, and pin
react-doctor to 0.9.12. The anti-slop task command documents concrete
good and bad fixes and forbids laundering types to satisfy a rule.
This commit is contained in:
Bohdan Triapitsyn
2026-08-16 15:55:08 +03:00
parent e3094ee676
commit 51aef5e316
30 changed files with 3512 additions and 33 deletions
+540
View File
@@ -0,0 +1,540 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
claimedFilePaths,
printClaims,
readActiveClaims,
releaseRun,
resolveRunsDir,
runDirPath,
} from "./lib/batch-claims.mjs";
const PIPELINE = "as";
// Resolved before command dispatch so every command shares one claims location.
const { runsDir: RUNS_DIR, shared: SHARED_CLAIMS } = resolveRunsDir(parseArgs(process.argv.slice(2))["claims-dir"]);
const DEFAULT_MAX_ACTIVE = 3;
const DEFAULT_CLAIM_TTL_DAYS = 3;
// Rules ordered by how mechanical and behavior-safe their fixes are. Higher
// scores are preferred when selecting the next batch.
const PRIORITY_RULES = new Map([
["no-object-parameters", 100],
["no-shape-in-symbol-names", 95],
["no-unknown-type-aliases", 90],
["no-unknown-returns", 85],
["no-unknown-parameters", 80],
["no-unsafe-dictionary-type", 75],
["no-conditional-empty-object-spread", 70],
["no-known-value-widening", 65],
["no-chained-type-assertions", 60],
["no-widen-then-assert", 55],
["no-reflect-get", 50],
["no-reflect-apply", 50],
["no-module-mocking", 30],
["require-safety-comment-for-type-assertion", 20],
["no-runtime-typeof", 10],
]);
// Excluded by default because they account for most of the existing backlog and
// their fixes are the least mechanical. Opt in with --include-noisy.
const NOISY_RULES = new Set(["no-runtime-typeof", "require-safety-comment-for-type-assertion"]);
function usage(exitCode = 0) {
const out = exitCode === 0 ? console.log : console.error;
out(`Usage:
bun run deslop -- next-batch [--min-issues 25] [--max-issues 60] [--max-files 4]
[--max-active ${DEFAULT_MAX_ACTIVE}] [--claim-ttl ${DEFAULT_CLAIM_TTL_DAYS}] [--include-noisy]
bun run deslop -- check-batch --run <run-id>
bun run deslop -- active [--claim-ttl ${DEFAULT_CLAIM_TTL_DAYS}]
Every command accepts --claims-dir <path> to isolate a working copy.
bun run deslop -- release --run <run-id>
bun run deslop -- file <path> [--include-noisy]
bun run deslop -- top [--limit 10] [--include-noisy]
Files selected by an active batch are excluded from later batches, so concurrent
batches never touch the same file, including batches created by the React Doctor
pipeline. Claims are shared across clones by default. A batch stays active until
it is released.
Examples:
bun run deslop -- next-batch --min-issues 25 --max-issues 60
bun run deslop -- file packages/ui/src/lib/settings/metadata.ts
bun run deslop -- check-batch --run 2026-08-16T10-12-44Z
bun run deslop -- release --run 2026-08-16T10-12-44Z`);
process.exit(exitCode);
}
function parseArgs(argv) {
const args = { _: [] };
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith("--")) {
args._.push(arg);
continue;
}
const key = arg.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith("--")) {
args[key] = true;
continue;
}
args[key] = next;
i += 1;
}
return args;
}
function asPositiveInt(value, fallback, name) {
if (value === undefined) return fallback;
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error(`Invalid --${name}: expected a positive integer.`);
}
return parsed;
}
function runOxlint() {
// Oxlint exits non-zero whenever it reports findings, so the report has to be
// read from stdout of the failed invocation rather than treated as an error.
let output;
try {
output = execFileSync("bunx", ["oxlint", "--format", "json"], {
cwd: process.cwd(),
encoding: "utf8",
maxBuffer: 256 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
});
} catch (error) {
// The utf8 encoding above makes stdout a string whenever the run produced
// a report; an empty stdout means the run itself failed.
if (!error.stdout) throw error;
output = error.stdout;
}
const report = JSON.parse(output);
return { diagnostics: normalizeDiagnostics(report.diagnostics ?? []) };
}
function ruleOf(code) {
const match = /^anti-slop\((.+)\)$/.exec(code ?? "");
return match ? match[1] : (code ?? "unknown");
}
function normalizeDiagnostics(rawDiagnostics) {
return rawDiagnostics.map((diagnostic) => {
const span = diagnostic.labels?.[0]?.span;
return {
filePath: diagnostic.filename,
rule: ruleOf(diagnostic.code),
severity: diagnostic.severity ?? "error",
message: diagnostic.message,
line: span?.line,
column: span?.column,
};
});
}
function selectableDiagnostics(report, includeNoisy) {
if (includeNoisy) return report.diagnostics;
return report.diagnostics.filter((diagnostic) => !NOISY_RULES.has(diagnostic.rule));
}
function groupByFile(diagnostics) {
const byFile = new Map();
for (const diagnostic of diagnostics) {
const list = byFile.get(diagnostic.filePath) ?? [];
list.push(diagnostic);
byFile.set(diagnostic.filePath, list);
}
return byFile;
}
function rulePriority(rule) {
return PRIORITY_RULES.get(rule) ?? 50;
}
function filePriority(diagnostics) {
const score = diagnostics.reduce((sum, diagnostic) => sum + rulePriority(diagnostic.rule), 0);
const mechanicalCount = diagnostics.filter((diagnostic) => rulePriority(diagnostic.rule) >= 75).length;
return score + mechanicalCount * 20;
}
function sortedFileEntries(diagnostics) {
return [...groupByFile(diagnostics).entries()].sort((a, b) => {
const scoreDiff = filePriority(b[1]) - filePriority(a[1]);
if (scoreDiff !== 0) return scoreDiff;
const countDiff = b[1].length - a[1].length;
if (countDiff !== 0) return countDiff;
return a[0].localeCompare(b[0]);
});
}
function summarizeRules(diagnostics) {
const counts = new Map();
for (const diagnostic of diagnostics) {
counts.set(diagnostic.rule, (counts.get(diagnostic.rule) ?? 0) + 1);
}
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}
function createRunId() {
return new Date().toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "Z");
}
function titleCase(value) {
return value
.replace(/[-_]+/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase())
.trim();
}
function fileNameWithoutExtension(filePath) {
const fileName = filePath.split("/").at(-1) ?? filePath;
return fileName.replace(/\.[^.]+$/, "");
}
function slugify(value) {
return value
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80);
}
function createBatchMetadata(runId, selectedFiles) {
const [datePart, timePart = ""] = runId.replace(/Z$/, "").split("T");
const timestamp = `${datePart.replace(/-/g, "")}-${timePart.replace(/-/g, "")}`;
const stems = selectedFiles.map((file) => fileNameWithoutExtension(file.filePath));
const readableArea = stems.length === 1
? stems[0]
: `${stems.slice(0, 2).join(" and ")}${stems.length > 2 ? ` plus ${stems.length - 2}` : ""}`;
const areaSlug = slugify(stems.slice(0, 3).join("-")) || "batch";
const batchName = `as-${timestamp}-${areaSlug}`;
return {
batchName,
branchName: `anti-slop/${batchName}`,
prTitle: `Reduce anti-slop findings in ${titleCase(readableArea)}`,
};
}
function selectBatch(entries, minIssues, maxIssues, maxFiles) {
if (entries.length === 0) {
return { selected: [], oversized: false, belowTarget: false, reason: "No findings available for selection." };
}
const firstFitting = entries.find(([, diagnostics]) => diagnostics.length >= minIssues && diagnostics.length <= maxIssues);
if (firstFitting) {
return {
selected: [firstFitting],
oversized: false,
belowTarget: false,
reason: "A prioritized file already fits the target window.",
};
}
const oversized = entries.find(([, diagnostics]) => diagnostics.length > maxIssues);
if (oversized) {
return {
selected: [oversized],
oversized: true,
belowTarget: false,
reason: "A prioritized file exceeds the target window and was selected as a single complete-file batch.",
};
}
const selected = [];
let total = 0;
for (const entry of entries) {
if (selected.length >= maxFiles) break;
const count = entry[1].length;
if (total + count > maxIssues) {
if (total >= minIssues) break;
continue;
}
selected.push(entry);
total += count;
if (total >= minIssues) break;
}
if (selected.length > 0) {
return {
selected,
oversized: false,
belowTarget: total < minIssues,
reason: total >= minIssues
? "Added complete files until the batch reached the target window."
: "No combination reached the minimum without exceeding the maximum; selected the best smaller complete-file batch.",
};
}
return {
selected: [entries[0]],
oversized: false,
belowTarget: entries[0][1].length < minIssues,
reason: "Selected the best available complete file below the target window.",
};
}
function writeRun(runId, payload) {
const dir = runDirPath(RUNS_DIR, PIPELINE, runId);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "baseline.json"), `${JSON.stringify(payload.report, null, 2)}\n`);
writeFileSync(join(dir, "batch.json"), `${JSON.stringify(payload.batch, null, 2)}\n`);
return dir;
}
function readRun(runId) {
const dir = runDirPath(RUNS_DIR, PIPELINE, runId);
const baselinePath = join(dir, "baseline.json");
const batchPath = join(dir, "batch.json");
if (!existsSync(baselinePath) || !existsSync(batchPath)) {
throw new Error(`Unknown run: ${runId}`);
}
return {
baseline: JSON.parse(readFileSync(baselinePath, "utf8")),
batch: JSON.parse(readFileSync(batchPath, "utf8")),
};
}
function printReportHeader(report) {
const total = report.diagnostics.length;
const affected = groupByFile(report.diagnostics).size;
const noisy = report.diagnostics.filter((diagnostic) => NOISY_RULES.has(diagnostic.rule)).length;
console.log(`Total findings: ${total} across ${affected} files`);
console.log(`Excluded-by-default findings: ${noisy} (${[...NOISY_RULES].join(", ")})`);
}
function commandNextBatch(args) {
const minIssues = asPositiveInt(args["min-issues"], 25, "min-issues");
const maxIssues = asPositiveInt(args["max-issues"], 60, "max-issues");
const maxFiles = asPositiveInt(args["max-files"], 4, "max-files");
if (minIssues > maxIssues) throw new Error("--min-issues cannot be greater than --max-issues.");
const maxActive = asPositiveInt(args["max-active"], DEFAULT_MAX_ACTIVE, "max-active");
const claimTtlDays = asPositiveInt(args["claim-ttl"], DEFAULT_CLAIM_TTL_DAYS, "claim-ttl");
const includeNoisy = args["include-noisy"] === true;
const claims = readActiveClaims(RUNS_DIR, claimTtlDays);
if (claims.length >= maxActive) {
console.log("Anti-Slop Next Batch");
console.log("");
console.log("NO BATCH AVAILABLE");
console.log(`Reason: ${claims.length} active batches already exist and the limit is ${maxActive}.`);
console.log("Stop here. Do not create a branch or a pull request.");
console.log("");
printClaims(claims, PIPELINE);
return;
}
const report = runOxlint();
const claimedPaths = claimedFilePaths(claims);
const candidates = selectableDiagnostics(report, includeNoisy)
.filter((diagnostic) => !claimedPaths.has(diagnostic.filePath));
const entries = sortedFileEntries(candidates);
if (entries.length === 0) {
console.log("Anti-Slop Next Batch");
console.log("");
console.log("NO BATCH AVAILABLE");
console.log("Reason: no unclaimed findings remain for the selected rules.");
console.log("Stop here. Do not create a branch or a pull request.");
console.log("");
printClaims(claims, PIPELINE);
return;
}
const selection = selectBatch(entries, minIssues, maxIssues, maxFiles);
const runId = createRunId();
const selectedFiles = selection.selected.map(([filePath, fileDiagnostics]) => ({
filePath,
diagnosticCount: fileDiagnostics.length,
rules: summarizeRules(fileDiagnostics),
}));
const metadata = createBatchMetadata(runId, selectedFiles);
const batch = {
runId,
...metadata,
minIssues,
maxIssues,
maxFiles,
maxActive,
includeNoisy,
selectedFiles,
oversized: selection.oversized,
belowTarget: selection.belowTarget,
reason: selection.reason,
};
const runDir = writeRun(runId, { report, batch });
console.log("Anti-Slop Next Batch");
console.log("");
console.log(`Run ID: ${runId}`);
console.log(`Batch name: ${batch.batchName}`);
console.log(`Branch name: ${batch.branchName}`);
console.log(`PR title: ${batch.prTitle}`);
console.log(`Baseline: ${join(runDir, "baseline.json")}`);
console.log(`Batch metadata: ${join(runDir, "batch.json")}`);
console.log("");
printReportHeader(report);
console.log("");
console.log(`Batch window: ${minIssues}-${maxIssues} findings`);
console.log(`Noisy rules included: ${includeNoisy ? "yes" : "no"}`);
console.log(`Active batches before this one: ${claims.length} of ${maxActive}`);
console.log(`Claims directory: ${RUNS_DIR} (${SHARED_CLAIMS ? "shared default" : "override"})`);
console.log(`Files excluded as claimed by active batches: ${claimedPaths.size}`);
console.log(`Selection mode: complete files only`);
console.log(`Batch total: ${selectedFiles.reduce((sum, file) => sum + file.diagnosticCount, 0)} findings`);
console.log(`Oversized: ${selection.oversized ? "yes" : "no"}`);
console.log(`Below target: ${selection.belowTarget ? "yes" : "no"}`);
console.log(`Selection reason: ${selection.reason}`);
console.log("");
console.log("Selected files:");
selection.selected.forEach(([filePath, fileDiagnostics], index) => {
console.log(`${index + 1}. ${filePath}`);
console.log(` Findings: ${fileDiagnostics.length}`);
console.log(" Rules:");
for (const [rule, count] of summarizeRules(fileDiagnostics)) {
console.log(` ${String(count).padStart(3)} ${rule}`);
}
console.log(" Findings detail:");
for (const diagnostic of fileDiagnostics) {
console.log(` line ${diagnostic.line ?? "?"}:${diagnostic.column ?? "?"} ${diagnostic.severity} ${diagnostic.rule}`);
console.log(` ${diagnostic.message}`);
}
console.log("");
});
}
function commandActive(args) {
const claimTtlDays = asPositiveInt(args["claim-ttl"], DEFAULT_CLAIM_TTL_DAYS, "claim-ttl");
console.log(`Claims directory: ${RUNS_DIR} (${SHARED_CLAIMS ? "shared default" : "override"})`);
printClaims(readActiveClaims(RUNS_DIR, claimTtlDays), PIPELINE);
}
function commandRelease(args) {
const runId = args.run;
if (!runId || runId === true) throw new Error("Missing --run <run-id>.");
const dir = releaseRun(RUNS_DIR, PIPELINE, runId);
console.log(`Released batch ${runId}`);
console.log(`Removed ${dir}`);
}
function commandTop(args) {
const limit = asPositiveInt(args.limit, 10, "limit");
const includeNoisy = args["include-noisy"] === true;
const report = runOxlint();
const entries = sortedFileEntries(selectableDiagnostics(report, includeNoisy)).slice(0, limit);
console.log(`Top ${limit} files by prioritized anti-slop findings`);
console.log("");
for (const [filePath, diagnostics] of entries) {
console.log(`${String(diagnostics.length).padStart(4)} ${filePath}`);
console.log(` ${summarizeRules(diagnostics).map(([rule, count]) => `${rule} ${count}`).join(", ")}`);
}
}
function commandFile(args) {
const filePath = args._[1];
if (!filePath) throw new Error("Missing file path. Usage: bun run deslop -- file <path>");
const includeNoisy = args["include-noisy"] === true;
const report = runOxlint();
const diagnostics = groupByFile(selectableDiagnostics(report, includeNoisy)).get(filePath) ?? [];
console.log(filePath);
console.log(`${diagnostics.length} findings`);
console.log("");
if (diagnostics.length === 0) return;
console.log("Rules:");
for (const [rule, count] of summarizeRules(diagnostics)) {
console.log(`${String(count).padStart(4)} ${rule}`);
}
console.log("");
console.log("Findings:");
for (const diagnostic of diagnostics) {
console.log(`line ${diagnostic.line ?? "?"}:${diagnostic.column ?? "?"} ${diagnostic.severity} ${diagnostic.rule}`);
console.log(` ${diagnostic.message}`);
}
}
function commandCheckBatch(args) {
const runId = args.run;
if (!runId || runId === true) throw new Error("Missing --run <run-id>.");
const { baseline, batch } = readRun(runId);
const current = runOxlint();
const includeNoisy = batch.includeNoisy === true;
const beforeDiagnostics = selectableDiagnostics(baseline, includeNoisy);
const afterDiagnostics = selectableDiagnostics(current, includeNoisy);
const beforeByFile = groupByFile(beforeDiagnostics);
const afterByFile = groupByFile(afterDiagnostics);
const selected = batch.selectedFiles ?? [];
let beforeTotal = 0;
let afterTotal = 0;
console.log("Anti-Slop Batch Check");
console.log("");
console.log(`Run ID: ${runId}`);
if (batch.batchName) console.log(`Batch name: ${batch.batchName}`);
if (batch.branchName) console.log(`Branch name: ${batch.branchName}`);
if (batch.prTitle) console.log(`PR title: ${batch.prTitle}`);
console.log("");
console.log("Selected files:");
for (const file of selected) {
const before = beforeByFile.get(file.filePath)?.length ?? 0;
const after = afterByFile.get(file.filePath)?.length ?? 0;
beforeTotal += before;
afterTotal += after;
console.log(file.filePath);
console.log(` Before: ${before}`);
console.log(` After: ${after}`);
console.log(` Delta: ${after - before}`);
}
const selectedPaths = new Set(selected.map((file) => file.filePath));
const beforeOutside = beforeDiagnostics.filter((diagnostic) => !selectedPaths.has(diagnostic.filePath)).length;
const afterOutside = afterDiagnostics.filter((diagnostic) => !selectedPaths.has(diagnostic.filePath)).length;
console.log("");
console.log("Batch result:");
console.log(`Fixed findings in selected files: ${Math.max(0, beforeTotal - afterTotal)}`);
console.log(`Remaining findings in selected files: ${afterTotal}`);
console.log(`Findings outside selected files delta: ${afterOutside - beforeOutside}`);
console.log("");
console.log("Current repository summary:");
printReportHeader(current);
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const command = args._[0];
if (!command || command === "help" || args.help) usage(0);
switch (command) {
case "next-batch":
commandNextBatch(args);
break;
case "top":
commandTop(args);
break;
case "file":
commandFile(args);
break;
case "check-batch":
commandCheckBatch(args);
break;
case "active":
commandActive(args);
break;
case "release":
commandRelease(args);
break;
default:
throw new Error(`Unknown command: ${command}`);
}
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});