#!/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.`);