design-agent
Version:
Universal AI Design Review Agent - CLI tool for scanning code for design drift
281 lines (221 loc) • 7.33 kB
JavaScript
/**
* GitHub suggested changes utility for PR comments
* Generates GitHub "suggested changes" markdown snippets for trivial fixes
*/
export function generateGitHubSuggestions(findings) {
const suggestions = [];
for (const finding of findings) {
if (finding.autofix && finding.autofix.before && finding.autofix.after) {
const suggestion = generateSuggestionSnippet(finding);
if (suggestion) {
suggestions.push(suggestion);
}
}
}
return suggestions;
}
export function generateSuggestionSnippet(finding) {
const { file, line, autofix, kind, severity } = finding;
// Only generate suggestions for high-confidence autofixes
if (autofix.confidence < 80) return null;
// Only suggest for certain types of findings
const suggestableTypes = [
'tokenDriftColor',
'spacingRawPx',
'inlineStyle',
'utilities',
'storybookProps'
];
if (!suggestableTypes.includes(kind)) return null;
// Generate the suggestion markdown
const suggestion = {
file,
line,
kind,
severity,
confidence: autofix.confidence,
description: autofix.description,
before: autofix.before,
after: autofix.after,
markdown: generateMarkdownSuggestion(file, line, autofix, finding)
};
return suggestion;
}
function generateMarkdownSuggestion(file, line, autofix, finding) {
const { before, after, description } = autofix;
const { kind, severity } = finding;
// Generate GitHub suggestion markdown
const suggestion = `\`\`\`suggestion
${after}
\`\`\`
**${description}**
- **Type:** ${kind}
- **Severity:** ${severity}
- **Confidence:** ${autofix.confidence}%`;
return suggestion;
}
export function generatePRComment(findings, options = {}) {
const {
includeSuggestions = true,
maxSuggestions = 10,
groupByFile = true
} = options;
let comment = '## 🤖 Design Review Agent\n\n';
// Summary
const summary = generateSummary(findings);
comment += summary + '\n\n';
// Suggestions
if (includeSuggestions) {
const suggestions = generateGitHubSuggestions(findings);
if (suggestions.length > 0) {
comment += '## 💡 Suggested Changes\n\n';
if (groupByFile) {
comment += generateGroupedSuggestions(suggestions.slice(0, maxSuggestions));
} else {
comment += generateFlatSuggestions(suggestions.slice(0, maxSuggestions));
}
}
}
// Additional findings
const nonSuggestableFindings = findings.filter(f =>
!f.autofix || f.autofix.confidence < 80
);
if (nonSuggestableFindings.length > 0) {
comment += '\n## 📋 Additional Findings\n\n';
comment += generateFindingsList(nonSuggestableFindings);
}
return comment;
}
function generateSummary(findings) {
const total = findings.length;
const critical = findings.filter(f => f.severity === 'critical').length;
const major = findings.filter(f => f.severity === 'major').length;
const minor = findings.filter(f => f.severity === 'minor').length;
const suggestions = findings.filter(f => f.autofix && f.autofix.confidence >= 80).length;
let summary = `Found **${total}** design issues:\n`;
summary += `- 🔴 **${critical}** critical\n`;
summary += `- 🟡 **${major}** major\n`;
summary += `- 🟢 **${minor}** minor\n`;
if (suggestions > 0) {
summary += `- 💡 **${suggestions}** with suggested fixes`;
}
return summary;
}
function generateGroupedSuggestions(suggestions) {
const grouped = {};
for (const suggestion of suggestions) {
if (!grouped[suggestion.file]) {
grouped[suggestion.file] = [];
}
grouped[suggestion.file].push(suggestion);
}
let markdown = '';
for (const [file, fileSuggestions] of Object.entries(grouped)) {
markdown += `### 📁 ${file}\n\n`;
for (const suggestion of fileSuggestions) {
markdown += `**Line ${suggestion.line}:** ${suggestion.description}\n\n`;
markdown += suggestion.markdown + '\n\n';
}
}
return markdown;
}
function generateFlatSuggestions(suggestions) {
let markdown = '';
for (const suggestion of suggestions) {
markdown += `### ${suggestion.file}:${suggestion.line}\n\n`;
markdown += suggestion.markdown + '\n\n';
}
return markdown;
}
function generateFindingsList(findings) {
let markdown = '';
for (const finding of findings) {
const emoji = getSeverityEmoji(finding.severity);
markdown += `${emoji} **${finding.kind}** - ${finding.msg}\n`;
markdown += ` *${finding.file}:${finding.line}*\n\n`;
}
return markdown;
}
function getSeverityEmoji(severity) {
switch (severity) {
case 'critical': return '🔴';
case 'major': return '🟡';
case 'minor': return '🟢';
default: return '⚪';
}
}
export function generateInlineComment(finding) {
if (!finding.autofix || finding.autofix.confidence < 80) {
return null;
}
const { autofix } = finding;
return {
path: finding.file,
line: finding.line,
body: `**${autofix.description}**
\`\`\`suggestion
${autofix.after}
\`\`\`
Confidence: ${autofix.confidence}%`,
start_line: finding.line,
start_side: 'RIGHT'
};
}
export function generateReviewComment(findings, options = {}) {
const {
includeSuggestions = true,
maxSuggestions = 5
} = options;
const suggestions = generateGitHubSuggestions(findings);
const comments = [];
for (const suggestion of suggestions.slice(0, maxSuggestions)) {
const comment = generateInlineComment({
...suggestion,
autofix: {
description: suggestion.description,
confidence: suggestion.confidence,
after: suggestion.after
}
});
if (comment) {
comments.push(comment);
}
}
return comments;
}
export function generateCommitMessage(findings) {
const critical = findings.filter(f => f.severity === 'critical').length;
const major = findings.filter(f => f.severity === 'major').length;
const minor = findings.filter(f => f.severity === 'minor').length;
let message = 'fix(design): ';
if (critical > 0) {
message += `fix ${critical} critical design issues`;
} else if (major > 0) {
message += `fix ${major} major design issues`;
} else if (minor > 0) {
message += `fix ${minor} minor design issues`;
} else {
message += 'address design findings';
}
return message;
}
export function generateChangelogEntry(findings) {
const suggestions = generateGitHubSuggestions(findings);
const critical = findings.filter(f => f.severity === 'critical').length;
const major = findings.filter(f => f.severity === 'major').length;
const minor = findings.filter(f => f.severity === 'minor').length;
let entry = '## Design Review Results\n\n';
entry += `- **${critical}** critical issues found\n`;
entry += `- **${major}** major issues found\n`;
entry += `- **${minor}** minor issues found\n`;
if (suggestions.length > 0) {
entry += `- **${suggestions.length}** suggested fixes available\n`;
}
if (suggestions.length > 0) {
entry += '\n### Suggested Fixes\n\n';
for (const suggestion of suggestions.slice(0, 5)) {
entry += `- ${suggestion.description} (${suggestion.file}:${suggestion.line})\n`;
}
}
return entry;
}