UNPKG

design-agent

Version:

Universal AI Design Review Agent - CLI tool for scanning code for design drift

296 lines (244 loc) 7.81 kB
import { readFile } from 'fs/promises'; import { join } from 'path'; export function applyFilters(findings, baseline, rules) { let filteredFindings = [...findings]; // Apply ignore patterns filteredFindings = applyIgnorePatterns(filteredFindings); // Apply baseline suppressions filteredFindings = applyBaselineSuppressions(filteredFindings, baseline); // Apply rule-based severity mapping filteredFindings = applyRuleSeverities(filteredFindings, rules); return filteredFindings; } export async function loadIgnorePatterns() { try { const ignoreContent = await readFile('.designagentignore', 'utf8'); return ignoreContent.split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); } catch (error) { return []; } } export function applyIgnorePatterns(findings) { return findings.filter(finding => { // Check if file should be ignored if (shouldIgnoreFile(finding.file)) { return false; } // Check if finding type should be ignored if (shouldIgnoreFindingType(finding.kind)) { return false; } return true; }); } function shouldIgnoreFile(filePath) { const ignorePatterns = [ 'node_modules/', '.git/', 'dist/', 'build/', '.next/', '.nuxt/', 'coverage/', '.nyc_output/', '*.min.js', '*.min.css', '*.bundle.js', '*.chunk.js' ]; return ignorePatterns.some(pattern => { if (pattern.includes('*')) { const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(filePath); } return filePath.includes(pattern); }); } function shouldIgnoreFindingType(kind) { const ignoredTypes = [ 'configError' // Ignore config errors by default ]; return ignoredTypes.includes(kind); } export function applyBaselineSuppressions(findings, baseline) { if (!baseline || !baseline.suppressions) { return findings; } return findings.filter(finding => { // Check if finding is suppressed in baseline const isSuppressed = baseline.suppressions.some(suppression => { // Check file match const fileMatches = !suppression.file || suppression.file === finding.file || (suppression.file.includes('*') && new RegExp(suppression.file.replace(/\*/g, '.*')).test(finding.file)); // Check kind match const kindMatches = !suppression.kind || suppression.kind === finding.kind; // Check if suppression has expired const notExpired = !suppression.expires || new Date(suppression.expires) > new Date(); return fileMatches && kindMatches && notExpired; }); return !isSuppressed; }); } export function applyRuleSeverities(findings, rules) { if (!rules) { return findings; } return findings.map(finding => { const ruleSeverity = rules[finding.kind]; if (ruleSeverity) { return { ...finding, severity: ruleSeverity }; } return finding; }); } export function createBaseline(findings, options = {}) { const baseline = { version: '1.0.0', createdAt: new Date().toISOString(), suppressions: [] }; // Add suppressions for findings that should be ignored for (const finding of findings) { if (shouldSuppressFinding(finding, options)) { baseline.suppressions.push({ file: finding.file, kind: finding.kind, reason: options.reason || 'Suppressed in baseline', expires: options.expires || null }); } } return baseline; } function shouldSuppressFinding(finding, options) { // Suppress findings based on options if (options.suppressMinor && finding.severity === 'minor') { return true; } if (options.suppressTypes && options.suppressTypes.includes(finding.kind)) { return true; } if (options.suppressFiles && options.suppressFiles.some(pattern => finding.file.includes(pattern) || new RegExp(pattern).test(finding.file) )) { return true; } return false; } export function validateBaseline(baseline) { const errors = []; if (!baseline.version) { errors.push('Missing version field'); } if (!baseline.createdAt) { errors.push('Missing createdAt field'); } if (!baseline.suppressions || !Array.isArray(baseline.suppressions)) { errors.push('Missing or invalid suppressions array'); } if (baseline.suppressions) { for (let i = 0; i < baseline.suppressions.length; i++) { const suppression = baseline.suppressions[i]; if (!suppression.file && !suppression.kind) { errors.push(`Suppression ${i} must have either file or kind specified`); } if (suppression.expires && isNaN(new Date(suppression.expires).getTime())) { errors.push(`Suppression ${i} has invalid expires date`); } } } return { valid: errors.length === 0, errors }; } export function mergeBaselines(baseline1, baseline2) { const merged = { version: '1.0.0', createdAt: new Date().toISOString(), suppressions: [] }; // Merge suppressions from both baselines if (baseline1.suppressions) { merged.suppressions.push(...baseline1.suppressions); } if (baseline2.suppressions) { merged.suppressions.push(...baseline2.suppressions); } // Remove duplicates merged.suppressions = merged.suppressions.filter((suppression, index, array) => { return array.findIndex(s => s.file === suppression.file && s.kind === suppression.kind ) === index; }); return merged; } export function generateIgnoreFile(findings, options = {}) { const ignorePatterns = []; // Add common ignore patterns ignorePatterns.push('# Design Agent Ignore File'); ignorePatterns.push('# Generated on ' + new Date().toISOString()); ignorePatterns.push(''); ignorePatterns.push('# Common directories to ignore'); ignorePatterns.push('node_modules/'); ignorePatterns.push('.git/'); ignorePatterns.push('dist/'); ignorePatterns.push('build/'); ignorePatterns.push('.next/'); ignorePatterns.push('.nuxt/'); ignorePatterns.push('coverage/'); ignorePatterns.push(''); ignorePatterns.push('# Generated files'); ignorePatterns.push('*.min.js'); ignorePatterns.push('*.min.css'); ignorePatterns.push('*.bundle.js'); ignorePatterns.push('*.chunk.js'); ignorePatterns.push(''); // Add file-specific patterns based on findings const filePatterns = new Set(); for (const finding of findings) { if (finding.file) { const dir = finding.file.substring(0, finding.file.lastIndexOf('/')); if (dir && !filePatterns.has(dir)) { filePatterns.add(dir); ignorePatterns.push(`# ${dir}/`); ignorePatterns.push(`${dir}/*.test.js`); ignorePatterns.push(`${dir}/*.spec.js`); } } } return ignorePatterns.join('\n'); } export function analyzeFiltering(findings, baseline, rules) { const analysis = { originalCount: findings.length, afterIgnore: 0, afterBaseline: 0, afterRules: 0, suppressions: 0, severityChanges: 0 }; // Apply filters step by step to analyze impact let filtered = applyIgnorePatterns(findings); analysis.afterIgnore = filtered.length; filtered = applyBaselineSuppressions(filtered, baseline); analysis.afterBaseline = filtered.length; analysis.suppressions = analysis.afterIgnore - analysis.afterBaseline; const beforeRules = filtered.map(f => f.severity); filtered = applyRuleSeverities(filtered, rules); analysis.afterRules = filtered.length; // Count severity changes for (let i = 0; i < filtered.length; i++) { if (beforeRules[i] !== filtered[i].severity) { analysis.severityChanges++; } } return analysis; }