/ scripts / sage-auto-fix.js
sage-auto-fix.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * Sage Auto-Fix Script
  5   *
  6   * Runs Sage AI review, then uses Claude CLI to automatically fix identified issues.
  7   * Creates fixes in a review branch for safety (doesn't commit directly to main).
  8   *
  9   * Workflow:
 10   * 1. Run quality-check to generate reports
 11   * 2. Parse ESLint, Prettier, Sage findings
 12   * 3. For each issue, use Claude CLI to generate fix
 13   * 4. Apply fixes to files
 14   * 5. Run tests to verify
 15   * 6. Commit to review branch if tests pass
 16   * 7. Roll back if tests fail
 17   *
 18   * Environment Variables:
 19   * - SAGE_AUTOFIX_MODEL: Model to use with claude CLI (default: sonnet)
 20   * - SAGE_AUTOFIX_BRANCH: Branch name (default: sage-autofix-YYYY-MM-DD)
 21   */
 22  
 23  import { execSync, execFileSync } from 'child_process';
 24  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
 25  import { join } from 'path';
 26  import { fileURLToPath } from 'url';
 27  import { dirname } from 'path';
 28  
 29  const __filename = fileURLToPath(import.meta.url);
 30  const __dirname = dirname(__filename);
 31  const projectRoot = join(__dirname, '..');
 32  
 33  // Configuration
 34  const MODEL = process.env.SAGE_AUTOFIX_MODEL || 'sonnet';
 35  const BRANCH_NAME =
 36    process.env.SAGE_AUTOFIX_BRANCH || `sage-autofix-${new Date().toISOString().split('T')[0]}`;
 37  const REPORT_DIR = join(projectRoot, '.quality-reports');
 38  const FIX_LOG_PATH = join(REPORT_DIR, 'auto-fix-log.json');
 39  
 40  // Stats tracking
 41  const stats = {
 42    startTime: new Date().toISOString(),
 43    issues: {
 44      eslint: 0,
 45      prettier: 0,
 46      sage: 0,
 47      total: 0,
 48    },
 49    fixes: {
 50      applied: 0,
 51      failed: 0,
 52      skipped: 0,
 53    },
 54    files: {
 55      modified: [],
 56    },
 57    branch: BRANCH_NAME,
 58    testsPassedAfterFixes: false,
 59    rollback: false,
 60  };
 61  
 62  console.log('๐Ÿค– Sage Auto-Fix Script\n');
 63  console.log(`Model: ${MODEL}`);
 64  console.log(`Branch: ${BRANCH_NAME}\n`);
 65  
 66  // Validate claude CLI is available
 67  try {
 68    execSync('which claude', { stdio: 'ignore' });
 69  } catch {
 70    console.error('Error: claude CLI not found in PATH');
 71    console.error('   Install at: https://claude.ai/claude-code');
 72    process.exit(1);
 73  }
 74  
 75  /**
 76   * Run quality checks and generate reports
 77   */
 78  function runQualityChecks() {
 79    console.log('๐Ÿ” Running quality checks...\n');
 80    try {
 81      execSync('npm run quality-check', {
 82        cwd: projectRoot,
 83        stdio: 'inherit',
 84      });
 85      console.log('โœ… Quality checks completed\n');
 86      return true;
 87    } catch (error) {
 88      // Quality check may exit with error if issues found - that's expected
 89      console.log('โš ๏ธ  Quality checks found issues - proceeding to fix them\n');
 90      return true;
 91    }
 92  }
 93  
 94  /**
 95   * Parse quality check reports
 96   */
 97  function parseReports() {
 98    const issues = [];
 99  
100    // Parse ESLint report
101    const eslintPath = join(REPORT_DIR, 'eslint.json');
102    if (existsSync(eslintPath)) {
103      const eslintReport = JSON.parse(readFileSync(eslintPath, 'utf-8'));
104      eslintReport.forEach(fileResult => {
105        if (fileResult.messages && fileResult.messages.length > 0) {
106          fileResult.messages.forEach(msg => {
107            issues.push({
108              type: 'eslint',
109              file: fileResult.filePath,
110              line: msg.line,
111              column: msg.column,
112              rule: msg.ruleId,
113              severity: msg.severity === 2 ? 'error' : 'warning',
114              message: msg.message,
115              fixable: msg.fix !== undefined,
116            });
117            stats.issues.eslint++;
118          });
119        }
120      });
121    }
122  
123    // Parse Sage report (if available)
124    const sagePath = join(REPORT_DIR, 'sage.json');
125    if (existsSync(sagePath)) {
126      try {
127        const sageReport = JSON.parse(readFileSync(sagePath, 'utf-8'));
128        // Sage format varies - adapt parsing based on actual structure
129        if (Array.isArray(sageReport.findings)) {
130          sageReport.findings.forEach(finding => {
131            issues.push({
132              type: 'sage',
133              file: finding.file || finding.filePath,
134              line: finding.line,
135              severity: finding.severity || 'warning',
136              message: finding.message || finding.description,
137              category: finding.category,
138            });
139            stats.issues.sage++;
140          });
141        }
142      } catch (error) {
143        console.log('โš ๏ธ  Could not parse Sage report:', error.message);
144      }
145    }
146  
147    stats.issues.total = issues.length;
148    return issues;
149  }
150  
151  /**
152   * Group issues by file
153   */
154  function groupIssuesByFile(issues) {
155    const grouped = {};
156    issues.forEach(issue => {
157      if (!grouped[issue.file]) {
158        grouped[issue.file] = [];
159      }
160      grouped[issue.file].push(issue);
161    });
162    return grouped;
163  }
164  
165  /**
166   * Use Claude API to fix issues in a file
167   */
168  async function fixFileIssues(filePath, issues) {
169    console.log(`\n๐Ÿ”ง Fixing ${issues.length} issues in ${filePath.replace(projectRoot, '.')}`);
170  
171    // Read current file content
172    let fileContent;
173    try {
174      fileContent = readFileSync(filePath, 'utf-8');
175    } catch (error) {
176      console.error(`   โŒ Could not read file: ${error.message}`);
177      stats.fixes.failed += issues.length;
178      return false;
179    }
180  
181    // Build prompt for Claude
182    const issuesDescription = issues
183      .map((issue, idx) => {
184        return `${idx + 1}. [Line ${issue.line || '?'}] ${issue.type.toUpperCase()}: ${issue.message}${issue.rule ? ` (${issue.rule})` : ''}`;
185      })
186      .join('\n');
187  
188    const prompt = `You are a code quality expert. Fix the following issues in this file:
189  
190  FILE: ${filePath.replace(projectRoot, '.')}
191  
192  ISSUES TO FIX:
193  ${issuesDescription}
194  
195  CURRENT FILE CONTENT:
196  \`\`\`javascript
197  ${fileContent}
198  \`\`\`
199  
200  INSTRUCTIONS:
201  - Fix ALL listed issues
202  - Maintain code functionality - only change what's necessary
203  - Follow existing code style and patterns
204  - If an issue is already fixed in the code, skip it
205  - Respond ONLY with the complete fixed file content, no explanations
206  - Do not add comments explaining what you fixed
207  - Preserve all existing comments and documentation
208  
209  OUTPUT THE COMPLETE FIXED FILE:`;
210  
211    try {
212      const fixedContent = execFileSync('claude', ['-p', '--model', MODEL, '--output-format', 'text'], {
213        input: prompt,
214        encoding: 'utf-8',
215        timeout: 120000,
216        maxBuffer: 10 * 1024 * 1024,
217      }).trim();
218  
219      // Basic validation: check if response looks like code
220      if (!fixedContent.includes('import') && !fixedContent.includes('function')) {
221        console.error('   โš ๏ธ  Claude response does not look like code - skipping');
222        stats.fixes.skipped += issues.length;
223        return false;
224      }
225  
226      // Write fixed content
227      writeFileSync(filePath, fixedContent);
228      stats.files.modified.push(filePath);
229      stats.fixes.applied += issues.length;
230  
231      console.log(`   โœ… Fixed ${issues.length} issues`);
232      return true;
233    } catch (error) {
234      console.error(`   โŒ Claude API error: ${error.message}`);
235      stats.fixes.failed += issues.length;
236      return false;
237    }
238  }
239  
240  /**
241   * Apply auto-fixable ESLint rules
242   */
243  function applyEslintAutofix() {
244    console.log('๐Ÿ”ง Applying ESLint auto-fix...\n');
245    try {
246      execSync('npm run lint:fix', {
247        cwd: projectRoot,
248        stdio: 'inherit',
249      });
250      console.log('โœ… ESLint auto-fix completed\n');
251      return true;
252    } catch (error) {
253      console.log('โš ๏ธ  Some ESLint issues may remain\n');
254      return false;
255    }
256  }
257  
258  /**
259   * Apply Prettier formatting
260   */
261  function applyPrettierFormat() {
262    console.log('๐Ÿ”ง Applying Prettier formatting...\n');
263    try {
264      execSync('npm run format', {
265        cwd: projectRoot,
266        stdio: 'inherit',
267      });
268      console.log('โœ… Prettier formatting completed\n');
269      return true;
270    } catch (error) {
271      console.error('โŒ Prettier formatting failed:', error.message);
272      return false;
273    }
274  }
275  
276  /**
277   * Run tests to verify fixes
278   */
279  function runTests() {
280    console.log('\n๐Ÿงช Running tests to verify fixes...\n');
281    try {
282      execSync('npm test', {
283        cwd: projectRoot,
284        stdio: 'inherit',
285      });
286      console.log('โœ… All tests passed!\n');
287      stats.testsPassedAfterFixes = true;
288      return true;
289    } catch (error) {
290      console.error('โŒ Tests failed after applying fixes!\n');
291      stats.testsPassedAfterFixes = false;
292      return false;
293    }
294  }
295  
296  /**
297   * Create git branch and commit fixes
298   */
299  function commitFixes() {
300    console.log('๐Ÿ“ Committing fixes to review branch...\n');
301  
302    try {
303      // Check if we're in a git repo
304      execSync('git rev-parse --git-dir', { cwd: projectRoot, stdio: 'ignore' });
305  
306      // Check if branch already exists
307      try {
308        execSync(`git rev-parse --verify ${BRANCH_NAME}`, { cwd: projectRoot, stdio: 'ignore' });
309        console.log(`โš ๏ธ  Branch ${BRANCH_NAME} already exists - using timestamp suffix`);
310        const timestamp = Date.now();
311        stats.branch = `${BRANCH_NAME}-${timestamp}`;
312      } catch {
313        // Branch doesn't exist - good to go
314      }
315  
316      // Create branch
317      execSync(`git checkout -b ${stats.branch}`, {
318        cwd: projectRoot,
319        stdio: 'inherit',
320      });
321  
322      // Stage modified files
323      stats.files.modified.forEach(file => {
324        execSync(`git add "${file}"`, { cwd: projectRoot });
325      });
326  
327      // Commit
328      const commitMessage = `fix: auto-fix quality issues via Sage AI review
329  
330  Applied ${stats.fixes.applied} fixes across ${stats.files.modified.length} files
331  
332  Issues fixed:
333  - ESLint: ${stats.issues.eslint} issues
334  - Sage AI: ${stats.issues.sage} issues
335  
336  All tests passing after fixes.
337  
338  Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`;
339  
340      execSync(`git commit -m "${commitMessage}"`, {
341        cwd: projectRoot,
342        stdio: 'inherit',
343      });
344  
345      console.log(`โœ… Fixes committed to branch: ${stats.branch}\n`);
346      console.log('๐Ÿ“‹ Next steps:');
347      console.log(`   1. Review changes: git diff main ${stats.branch}`);
348      console.log(`   2. Merge if satisfied: git checkout main && git merge ${stats.branch}`);
349      console.log(`   3. Delete branch: git branch -d ${stats.branch}\n`);
350  
351      return true;
352    } catch (error) {
353      console.error('โŒ Git commit failed:', error.message);
354      return false;
355    }
356  }
357  
358  /**
359   * Roll back changes
360   */
361  function rollback() {
362    console.log('\nโš ๏ธ  Rolling back changes due to test failures...\n');
363    stats.rollback = true;
364  
365    try {
366      // Reset modified files
367      stats.files.modified.forEach(file => {
368        execSync(`git checkout HEAD -- "${file}"`, { cwd: projectRoot });
369      });
370      console.log('โœ… Rollback complete - files restored to original state\n');
371    } catch (error) {
372      console.error('โŒ Rollback failed:', error.message);
373      console.error('   You may need to manually restore files with: git checkout HEAD -- .');
374    }
375  }
376  
377  /**
378   * Save fix log
379   */
380  function saveLogs() {
381    stats.endTime = new Date().toISOString();
382    const duration = new Date(stats.endTime) - new Date(stats.startTime);
383    stats.durationSeconds = (duration / 1000).toFixed(2);
384  
385    writeFileSync(FIX_LOG_PATH, JSON.stringify(stats, null, 2));
386    console.log(`\n๐Ÿ“Š Fix log saved: ${FIX_LOG_PATH.replace(projectRoot, '.')}\n`);
387  }
388  
389  /**
390   * Main execution
391   */
392  async function main() {
393    // 1. Run quality checks
394    runQualityChecks();
395  
396    // 2. Parse reports
397    console.log('๐Ÿ“‹ Parsing quality reports...\n');
398    const issues = parseReports();
399  
400    if (issues.length === 0) {
401      console.log('โœ… No issues found - code quality is excellent!\n');
402      stats.endTime = new Date().toISOString();
403      saveLogs();
404      return;
405    }
406  
407    console.log(`Found ${stats.issues.total} total issues:`);
408    console.log(`   - ESLint: ${stats.issues.eslint}`);
409    console.log(`   - Sage AI: ${stats.issues.sage}\n`);
410  
411    // 3. Apply auto-fixable rules first
412    applyEslintAutofix();
413    applyPrettierFormat();
414  
415    // 4. Parse reports again to see what remains
416    console.log('๐Ÿ“‹ Re-checking after auto-fixes...\n');
417    const remainingIssues = parseReports();
418  
419    if (remainingIssues.length === 0) {
420      console.log('โœ… All issues resolved with auto-fix!\n');
421  
422      // Verify with tests
423      const testsPass = runTests();
424      if (testsPass) {
425        commitFixes();
426      } else {
427        rollback();
428      }
429  
430      saveLogs();
431      return;
432    }
433  
434    console.log(`${remainingIssues.length} issues require AI-assisted fixes\n`);
435  
436    // 5. Group by file and fix with Claude
437    const groupedIssues = groupIssuesByFile(remainingIssues);
438    const filesToFix = Object.keys(groupedIssues);
439  
440    console.log(`Fixing issues across ${filesToFix.length} files...\n`);
441  
442    for (const filePath of filesToFix) {
443      const fileIssues = groupedIssues[filePath];
444      await fixFileIssues(filePath, fileIssues);
445    }
446  
447    // 6. Apply formatting after AI fixes
448    applyPrettierFormat();
449  
450    // 7. Run tests
451    const testsPass = runTests();
452  
453    // 8. Commit or rollback
454    if (testsPass && stats.fixes.applied > 0) {
455      commitFixes();
456    } else if (!testsPass) {
457      rollback();
458    } else {
459      console.log('โš ๏ธ  No fixes were applied\n');
460    }
461  
462    // 9. Save logs
463    saveLogs();
464  
465    // Print summary
466    console.log('โ•'.repeat(60));
467    console.log('\n๐Ÿ“Š Auto-Fix Summary\n');
468    console.log('โ•'.repeat(60));
469    console.log(`Total Issues Found: ${stats.issues.total}`);
470    console.log(`Fixes Applied: ${stats.fixes.applied}`);
471    console.log(`Fixes Failed: ${stats.fixes.failed}`);
472    console.log(`Fixes Skipped: ${stats.fixes.skipped}`);
473    console.log(`Files Modified: ${stats.files.modified.length}`);
474    console.log(`Tests Passed: ${stats.testsPassedAfterFixes ? 'โœ… Yes' : 'โŒ No'}`);
475    console.log(`Rollback: ${stats.rollback ? 'โš ๏ธ  Yes' : 'โœ… No'}`);
476    console.log(`Branch: ${stats.branch}`);
477    console.log(`Duration: ${stats.durationSeconds}s`);
478    console.log(`\n${'โ•'.repeat(60)}\n`);
479  
480    // Exit with appropriate code
481    process.exit(stats.testsPassedAfterFixes && stats.fixes.applied > 0 ? 0 : 1);
482  }
483  
484  // Run
485  main().catch(error => {
486    console.error('โŒ Fatal error:', error);
487    stats.error = error.message;
488    stats.endTime = new Date().toISOString();
489    saveLogs();
490    process.exit(1);
491  });