From 776c5cba4c545767ba7017fa8385f6ccbd08b65e Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 6 Oct 2026 12:57:45 +0200 Subject: [PATCH] feat(qa): automatic sonar-fixer loop (file, deliver, verify, repeat) with safety stops Co-Authored-By: Claude Sonnet 5.5 --- scripts/sonar_auto.mjs | 47 ++++++++++++++++++++++++++++ scripts/sonar_triage.defer.json | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 scripts/sonar_auto.mjs create mode 100644 scripts/sonar_triage.defer.json diff --git a/scripts/sonar_auto.mjs b/scripts/sonar_auto.mjs new file mode 100644 index 0000000..10a5308 --- /dev/null +++ b/scripts/sonar_auto.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node +// Automatic sonar-fixer loop: file a batch -> deliver it one issue at a time (--drain) -> verify -> repeat. +// Stops (never pushes, never reverts) when: an issue stalls, the quick QA fails, a fix commit looks too big, the tree gets dirty, +// nothing is left to file, or --batches is reached. Log: progress lines on stdout; run it detached: `nohup node scripts/sonar_auto.mjs &`. +import { readFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const arg = (n, d) => { const i = process.argv.indexOf(n); return i > 0 ? process.argv[i + 1] : d; }; +const BATCHES = +arg('--batches', 20), CAP = +arg('--cap', 5), TIMEOUT = +arg('--timeout', 60), MAX_FILES = +arg('--max-files', 25); +const defer = JSON.parse(readFileSync(join(ROOT, 'scripts', 'sonar_triage.defer.json'), 'utf8')).rules.join(','); +const log = (m) => console.log(`[${new Date().toISOString().slice(11, 19)}] ${m}`); +const run = (cmd, args, o = {}) => spawnSync(cmd, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 << 20, ...o }); +const triage = (...a) => run('node', ['scripts/sonar_triage.mjs', ...a], { timeout: 4 * 3600 * 1000 }); +const dirtyTracked = () => run('git', ['status', '--short']).stdout.split('\n').filter((l) => l && !l.startsWith('??')).length; +const openSonar = () => JSON.parse(run('gh', ['issue', 'list', '--state', 'open', '--label', 'sonar', '--label', 'agent-fix', '--json', 'number']).stdout || '[]').length; +const head = () => run('git', ['rev-parse', 'HEAD']).stdout.trim(); + +function stop(why) { log(`STOPPED: ${why}`); process.exit(2); } + +for (let b = 1; b <= BATCHES; b++) { + if (dirtyTracked()) stop('tracked working tree is dirty (an agent is mid-edit or left changes); clean it, then rerun'); + const before = head(); + if (openSonar() === 0) { + const f = triage('--all', '--cap', String(CAP), '--apply', '--no-webhook', '--exclude-rules', defer); + const created = (f.stdout.match(/created https:\/\/\S+/g) || []).length; + log(`batch ${b}: filed ${created} issue(s)`); + if (!created) { log('nothing left to file for this lane. DONE.'); process.exit(0); } + } else log(`batch ${b}: open issues from an earlier run, delivering those first`); + const d = triage('--apply', '--drain', '--timeout', String(TIMEOUT)); + log(d.stdout.trim().split('\n').slice(-6).join(' | ')); + if (/stalled/.test(d.stdout)) stop('an issue stalled (no answer after a resend); look at multica, then rerun'); + const commits = run('git', ['log', '--format=%h', `${before}..HEAD`]).stdout.split('\n').filter(Boolean); + for (const c of commits) { + const n = run('git', ['show', '--name-only', '--format=', c]).stdout.split('\n').filter(Boolean).length; + log(` commit ${c}: ${n} file(s) - ${run('git', ['show', '-s', '--format=%s', c]).stdout.trim().slice(0, 90)}`); + if (n > MAX_FILES) stop(`commit ${c} touches ${n} files (> ${MAX_FILES}); review it by hand`); + } + if (dirtyTracked()) stop('tree is dirty after the batch'); + const q = run('node', ['scripts/qa.mjs', '--fast'], { timeout: 30 * 60 * 1000 }); + const ok = q.status === 0; + log(`batch ${b}: quick QA ${ok ? 'OK' : 'FAILED'}`); + if (!ok) stop('quick QA failed after the batch; see `node scripts/qa.mjs --fast`; no revert was done'); +} +log(`DONE: ${BATCHES} batches processed.`); diff --git a/scripts/sonar_triage.defer.json b/scripts/sonar_triage.defer.json new file mode 100644 index 0000000..2dcb983 --- /dev/null +++ b/scripts/sonar_triage.defer.json @@ -0,0 +1,55 @@ +{ + "_comment": "Rules the automatic loop must NOT file for the sonar-fixer lane: design-sensitive, async/security, or low value (see architecture/sonar-triage.md). Complexity (S3776) has its own lane.", + "rules": [ + "python:S3776", + "javascript:S3776", + "python:S1192", + "javascript:S1192", + "python:S8415", + "javascript:S8415", + "python:S3358", + "javascript:S3358", + "python:S5850", + "javascript:S5850", + "python:S2189", + "javascript:S2189", + "python:S7488", + "javascript:S7488", + "python:S7493", + "javascript:S7493", + "python:S7499", + "javascript:S7499", + "python:S7503", + "javascript:S7503", + "python:S5443", + "javascript:S5443", + "python:S2004", + "javascript:S2004", + "python:S2703", + "javascript:S2703", + "python:S3516", + "javascript:S3516", + "python:S1313", + "javascript:S1313", + "python:S8410", + "javascript:S8410", + "python:S2486", + "javascript:S2486", + "python:S2681", + "javascript:S2681", + "python:S1874", + "javascript:S1874", + "python:S8786", + "javascript:S8786", + "python:S5843", + "javascript:S5843", + "python:S2925", + "javascript:S2925", + "python:S7721", + "javascript:S7721", + "python:S5781", + "javascript:S5781", + "python:S4624", + "javascript:S4624" + ] +} \ No newline at end of file