design-agent
Version:
Universal AI Design Review Agent - CLI tool for scanning code for design drift
296 lines (244 loc) • 7.81 kB
JavaScript
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;
}