diff --git a/scripts/slop-diff.ts b/scripts/slop-diff.ts index 87eaf84a..b2a5abd1 100644 --- a/scripts/slop-diff.ts +++ b/scripts/slop-diff.ts @@ -11,48 +11,55 @@ * bun run slop:diff origin/release # diff against another base */ -import { spawnSync } from 'child_process'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; +import { spawnSync } from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; -const base = process.argv[2] || 'main'; +const base = process.argv[2] || "main"; // 1. Find changed files -const diffResult = spawnSync('git', ['diff', '--name-only', `${base}...HEAD`], { - encoding: 'utf-8', timeout: 10000, +const diffResult = spawnSync("git", ["diff", "--name-only", `${base}...HEAD`], { + encoding: "utf-8", + timeout: 10000, }); const changedFiles = new Set( - (diffResult.stdout || '').trim().split('\n').filter(Boolean) + (diffResult.stdout || "").trim().split("\n").filter(Boolean), ); if (changedFiles.size === 0) { - console.log('No files changed vs', base, '— nothing to check.'); + console.log("No files changed vs", base, "— nothing to check."); process.exit(0); } // 2. Run slop-scan on HEAD -const scanHead = spawnSync('npx', ['slop-scan', 'scan', '.', '--json'], { - encoding: 'utf-8', timeout: 120000, shell: true, +const scanHead = spawnSync("npx", ["slop-scan", "scan", ".", "--json"], { + encoding: "utf-8", + timeout: 120000, + shell: process.platform === "win32", }); if (!scanHead.stdout) { - console.log('slop-scan not available. Install: npm i -g slop-scan'); + console.log("slop-scan not available. Install: npm i -g slop-scan"); process.exit(0); } let headReport: any; -try { headReport = JSON.parse(scanHead.stdout); } catch { - console.log('slop-scan returned invalid JSON.'); process.exit(0); +try { + headReport = JSON.parse(scanHead.stdout); +} catch { + console.log("slop-scan returned invalid JSON."); + process.exit(0); } // 3. Get base branch findings using git stash approach // Check out base versions of changed files, scan, then restore -const mergeBase = spawnSync('git', ['merge-base', base, 'HEAD'], { - encoding: 'utf-8', timeout: 5000, +const mergeBase = spawnSync("git", ["merge-base", base, "HEAD"], { + encoding: "utf-8", + timeout: 5000, }).stdout?.trim(); // Fingerprint: strip line numbers so shifting code doesn't create false positives // "line 142: empty catch, boundary=none" -> "empty catch, boundary=none" function stripLineNum(evidence: string): string { - return evidence.replace(/^line \d+: /, '').replace(/ at line \d+ /, ' '); + return evidence.replace(/^line \d+: /, "").replace(/ at line \d+ /, " "); } // Count evidence items per (rule, file, stripped-evidence) for the base @@ -61,27 +68,40 @@ const baseCounts = new Map(); if (mergeBase) { // Create temp worktree for base scan const tmpWorktree = path.join(os.tmpdir(), `slop-base-${Date.now()}`); - const wtResult = spawnSync('git', ['worktree', 'add', '--detach', tmpWorktree, mergeBase], { - encoding: 'utf-8', timeout: 30000, - }); + const wtResult = spawnSync( + "git", + ["worktree", "add", "--detach", tmpWorktree, mergeBase], + { + encoding: "utf-8", + timeout: 30000, + }, + ); if (wtResult.status === 0) { // Copy slop-scan config if it exists - const configFile = 'slop-scan.config.json'; + const configFile = "slop-scan.config.json"; if (fs.existsSync(configFile)) { - try { fs.copyFileSync(configFile, path.join(tmpWorktree, configFile)); } catch {} + try { + fs.copyFileSync(configFile, path.join(tmpWorktree, configFile)); + } catch {} } - const scanBase = spawnSync('npx', ['slop-scan', 'scan', tmpWorktree, '--json'], { - encoding: 'utf-8', timeout: 120000, shell: true, - }); + const scanBase = spawnSync( + "npx", + ["slop-scan", "scan", tmpWorktree, "--json"], + { + encoding: "utf-8", + timeout: 120000, + shell: process.platform === "win32", + }, + ); if (scanBase.stdout) { try { const baseReport = JSON.parse(scanBase.stdout); for (const f of baseReport.findings) { // Remap worktree paths back to repo-relative - const realPath = f.path.replace(tmpWorktree + '/', ''); + const realPath = f.path.replace(tmpWorktree + "/", ""); if (!changedFiles.has(realPath)) continue; for (const ev of f.evidence || []) { const key = `${f.ruleId}|${realPath}|${stripLineNum(ev)}`; @@ -92,7 +112,7 @@ if (mergeBase) { } // Clean up worktree - spawnSync('git', ['worktree', 'remove', '--force', tmpWorktree], { + spawnSync("git", ["worktree", "remove", "--force", tmpWorktree], { timeout: 10000, }); } @@ -102,7 +122,9 @@ if (mergeBase) { // For each evidence item on HEAD, check if the base had the same (rule, file, stripped-evidence). // Use counts to handle duplicates: if base had 2 and HEAD has 3, that's 1 new. const headCounts = new Map(); -const headFindings = headReport.findings.filter((f: any) => changedFiles.has(f.path)); +const headFindings = headReport.findings.filter((f: any) => + changedFiles.has(f.path), +); for (const f of headFindings) { for (const ev of f.evidence || []) { @@ -123,7 +145,7 @@ for (const [key, entry] of headCounts) { const baseCount = baseCounts.get(key) || 0; const netNew = entry.count - baseCount; if (netNew > 0) { - const [ruleId, filePath] = key.split('|'); + const [ruleId, filePath] = key.split("|"); // Take the last N evidence items as the "new" ones for (const ev of entry.evidence.slice(-netNew)) { newFindings.push({ ruleId, filePath, evidence: ev }); @@ -139,14 +161,20 @@ for (const [key, baseCount] of baseCounts) { // 5. Print results if (newFindings.length === 0) { if (removedCount > 0) { - console.log(`\n slop-scan: no new findings. Removed ${removedCount} pre-existing findings.\n`); + console.log( + `\n slop-scan: no new findings. Removed ${removedCount} pre-existing findings.\n`, + ); } else { - console.log(`\n slop-scan: no new findings in ${changedFiles.size} changed files.\n`); + console.log( + `\n slop-scan: no new findings in ${changedFiles.size} changed files.\n`, + ); } process.exit(0); } -console.log(`\n── slop-scan: ${newFindings.length} new findings (+${newFindings.length} / -${removedCount}) ──\n`); +console.log( + `\n── slop-scan: ${newFindings.length} new findings (+${newFindings.length} / -${removedCount}) ──\n`, +); // Group by file, then by rule const grouped = new Map>();