design-agent
Version:
Universal AI Design Review Agent - CLI tool for scanning code for design drift
240 lines (209 loc) • 7.42 kB
JavaScript
export function generateSARIF(findings) {
return {
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
version: '2.1.0',
runs: [{
tool: {
driver: {
name: 'Design Agent',
version: '1.0.0',
informationUri: 'https://github.com/ollyaston/design-agent',
rules: generateRules(findings)
}
},
results: generateResults(findings),
invocations: [{
executionSuccessful: true,
startTimeUtc: new Date().toISOString(),
endTimeUtc: new Date().toISOString()
}]
}]
};
}
function generateRules(findings) {
const ruleIds = [...new Set(findings.map(f => f.kind))];
return ruleIds.map(ruleId => ({
id: ruleId,
name: getRuleName(ruleId),
shortDescription: {
text: getRuleDescription(ruleId)
},
fullDescription: {
text: getRuleFullDescription(ruleId)
},
help: {
text: getRuleHelp(ruleId)
},
properties: {
category: getRuleCategory(ruleId),
severity: getRuleSeverity(ruleId)
}
}));
}
function generateResults(findings) {
return findings.map(finding => ({
ruleId: finding.kind,
level: mapSeverityToLevel(finding.severity),
message: {
text: finding.msg
},
locations: [{
physicalLocation: {
artifactLocation: {
uri: finding.file
},
region: {
startLine: finding.line,
endLine: finding.line
}
}
}],
properties: {
value: finding.value,
autofix: finding.autofix || null
}
}));
}
function getRuleName(ruleId) {
const ruleNames = {
'tokenDriftColor': 'Design Token Color Drift',
'spacingRawPx': 'Raw Pixel Spacing',
'inlineStyle': 'Inline Styles',
'utilities': 'Utility Class Issues',
'typography': 'Typography Issues',
'accessibility': 'Accessibility Issues',
'designSystem': 'Design System Violations',
'configError': 'Configuration Error',
'contrastFail': 'Color Contrast Failure'
};
return ruleNames[ruleId] || ruleId;
}
function getRuleDescription(ruleId) {
const descriptions = {
'tokenDriftColor': 'Hardcoded color values that should use design tokens',
'spacingRawPx': 'Raw pixel values that should use design tokens',
'inlineStyle': 'Inline styles that should use CSS classes or design tokens',
'utilities': 'Issues with utility classes or CSS utilities',
'typography': 'Typography-related design issues',
'accessibility': 'Accessibility compliance issues',
'designSystem': 'Design system consistency violations',
'configError': 'Configuration file parsing errors',
'contrastFail': 'Color contrast accessibility failures'
};
return descriptions[ruleId] || 'Design review finding';
}
function getRuleFullDescription(ruleId) {
const fullDescriptions = {
'tokenDriftColor': 'This rule detects hardcoded color values (hex, rgb, etc.) that should be replaced with design tokens for consistency and maintainability.',
'spacingRawPx': 'This rule detects raw pixel values that should be replaced with design tokens for responsive design and consistency.',
'inlineStyle': 'This rule detects inline styles that should be moved to CSS classes or design tokens for better maintainability.',
'utilities': 'This rule detects issues with utility classes, such as redundant or conflicting classes.',
'typography': 'This rule detects typography-related issues, such as inconsistent font usage or missing design tokens.',
'accessibility': 'This rule detects accessibility issues, such as missing alt text or insufficient color contrast.',
'designSystem': 'This rule detects violations of design system guidelines and consistency rules.',
'configError': 'This rule detects errors in configuration files that prevent proper design token usage.',
'contrastFail': 'This rule detects color contrast issues that fail accessibility guidelines.'
};
return fullDescriptions[ruleId] || 'Design review finding that requires attention.';
}
function getRuleHelp(ruleId) {
const helpTexts = {
'tokenDriftColor': 'Replace hardcoded color values with design tokens from your design system.',
'spacingRawPx': 'Replace raw pixel values with design tokens for consistent spacing.',
'inlineStyle': 'Move inline styles to CSS classes or use design tokens.',
'utilities': 'Review and optimize utility class usage.',
'typography': 'Use consistent typography tokens from your design system.',
'accessibility': 'Ensure proper accessibility attributes and contrast ratios.',
'designSystem': 'Follow your design system guidelines and naming conventions.',
'configError': 'Fix configuration file syntax or structure issues.',
'contrastFail': 'Improve color contrast to meet accessibility standards.'
};
return helpTexts[ruleId] || 'Review the finding and apply appropriate fixes.';
}
function getRuleCategory(ruleId) {
const categories = {
'tokenDriftColor': 'design-tokens',
'spacingRawPx': 'design-tokens',
'inlineStyle': 'code-style',
'utilities': 'code-style',
'typography': 'design-tokens',
'accessibility': 'accessibility',
'designSystem': 'design-system',
'configError': 'configuration',
'contrastFail': 'accessibility'
};
return categories[ruleId] || 'design-review';
}
function getRuleSeverity(ruleId) {
const severities = {
'tokenDriftColor': 'major',
'spacingRawPx': 'major',
'inlineStyle': 'minor',
'utilities': 'minor',
'typography': 'minor',
'accessibility': 'major',
'designSystem': 'minor',
'configError': 'minor',
'contrastFail': 'critical'
};
return severities[ruleId] || 'minor';
}
function mapSeverityToLevel(severity) {
const levelMap = {
'critical': 'error',
'major': 'error',
'minor': 'warning',
'info': 'note'
};
return levelMap[severity] || 'warning';
}
export function generateSARIFSummary(findings) {
const summary = {
totalFindings: findings.length,
bySeverity: {},
byRule: {},
byFile: {}
};
// Count by severity
findings.forEach(finding => {
const severity = finding.severity || 'minor';
summary.bySeverity[severity] = (summary.bySeverity[severity] || 0) + 1;
});
// Count by rule
findings.forEach(finding => {
const rule = finding.kind || 'unknown';
summary.byRule[rule] = (summary.byRule[rule] || 0) + 1;
});
// Count by file
findings.forEach(finding => {
const file = finding.file || 'unknown';
summary.byFile[file] = (summary.byFile[file] || 0) + 1;
});
return summary;
}
export function validateSARIF(sarif) {
const errors = [];
// Check required fields
if (!sarif.$schema) {
errors.push('Missing $schema field');
}
if (!sarif.version) {
errors.push('Missing version field');
}
if (!sarif.runs || !Array.isArray(sarif.runs)) {
errors.push('Missing or invalid runs array');
}
if (sarif.runs && sarif.runs.length > 0) {
const run = sarif.runs[0];
if (!run.tool || !run.tool.driver) {
errors.push('Missing tool.driver in run');
}
if (!run.results || !Array.isArray(run.results)) {
errors.push('Missing or invalid results array');
}
}
return {
valid: errors.length === 0,
errors
};
}