Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
418
tools/analyze/quick_wins.py
Normal file
418
tools/analyze/quick_wins.py
Normal file
@@ -0,0 +1,418 @@
|
||||
"""
|
||||
Quick-Win Finder
|
||||
|
||||
Identifies easy improvement opportunities in a codebase:
|
||||
- Inline styles that can be extracted
|
||||
- Duplicate values that should be tokens
|
||||
- Unused styles
|
||||
- Naming inconsistencies
|
||||
- Accessibility issues
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .base import (
|
||||
QuickWin,
|
||||
QuickWinType,
|
||||
QuickWinPriority,
|
||||
Location,
|
||||
ProjectAnalysis,
|
||||
)
|
||||
from .styles import StyleAnalyzer
|
||||
from .react import ReactAnalyzer
|
||||
|
||||
|
||||
class QuickWinFinder:
|
||||
"""
|
||||
Finds quick improvement opportunities in a project.
|
||||
|
||||
Categories:
|
||||
- INLINE_STYLE: Inline styles that can be extracted to CSS/tokens
|
||||
- DUPLICATE_VALUE: Repeated values that should be tokens
|
||||
- UNUSED_STYLE: CSS that's defined but not used
|
||||
- HARDCODED_VALUE: Magic numbers/colors that should be tokens
|
||||
- NAMING_INCONSISTENCY: Inconsistent naming patterns
|
||||
- DEPRECATED_PATTERN: Outdated styling approaches
|
||||
- ACCESSIBILITY: A11y improvements
|
||||
"""
|
||||
|
||||
def __init__(self, root_path: str):
|
||||
self.root = Path(root_path).resolve()
|
||||
self.style_analyzer = StyleAnalyzer(root_path)
|
||||
self.react_analyzer = ReactAnalyzer(root_path)
|
||||
|
||||
async def find_all(self) -> List[QuickWin]:
|
||||
"""
|
||||
Find all quick-win opportunities.
|
||||
|
||||
Returns:
|
||||
List of QuickWin objects sorted by priority
|
||||
"""
|
||||
quick_wins = []
|
||||
|
||||
# Find inline styles
|
||||
inline_wins = await self._find_inline_style_wins()
|
||||
quick_wins.extend(inline_wins)
|
||||
|
||||
# Find duplicate values
|
||||
duplicate_wins = await self._find_duplicate_value_wins()
|
||||
quick_wins.extend(duplicate_wins)
|
||||
|
||||
# Find unused styles
|
||||
unused_wins = await self._find_unused_style_wins()
|
||||
quick_wins.extend(unused_wins)
|
||||
|
||||
# Find hardcoded values
|
||||
hardcoded_wins = await self._find_hardcoded_value_wins()
|
||||
quick_wins.extend(hardcoded_wins)
|
||||
|
||||
# Find naming inconsistencies
|
||||
naming_wins = await self._find_naming_inconsistency_wins()
|
||||
quick_wins.extend(naming_wins)
|
||||
|
||||
# Find accessibility issues
|
||||
a11y_wins = await self._find_accessibility_wins()
|
||||
quick_wins.extend(a11y_wins)
|
||||
|
||||
# Sort by priority
|
||||
priority_order = {
|
||||
QuickWinPriority.CRITICAL: 0,
|
||||
QuickWinPriority.HIGH: 1,
|
||||
QuickWinPriority.MEDIUM: 2,
|
||||
QuickWinPriority.LOW: 3,
|
||||
}
|
||||
quick_wins.sort(key=lambda x: priority_order[x.priority])
|
||||
|
||||
return quick_wins
|
||||
|
||||
async def _find_inline_style_wins(self) -> List[QuickWin]:
|
||||
"""Find inline styles that should be extracted."""
|
||||
wins = []
|
||||
|
||||
inline_styles = await self.react_analyzer.find_inline_styles()
|
||||
|
||||
if not inline_styles:
|
||||
return wins
|
||||
|
||||
# Group by file
|
||||
by_file = {}
|
||||
for style in inline_styles:
|
||||
file_path = style['file']
|
||||
if file_path not in by_file:
|
||||
by_file[file_path] = []
|
||||
by_file[file_path].append(style)
|
||||
|
||||
# Create quick-wins for files with multiple inline styles
|
||||
for file_path, styles in by_file.items():
|
||||
if len(styles) >= 3: # Only flag if 3+ inline styles
|
||||
wins.append(QuickWin(
|
||||
type=QuickWinType.INLINE_STYLE,
|
||||
priority=QuickWinPriority.HIGH,
|
||||
title=f"Extract {len(styles)} inline styles",
|
||||
description=f"File {file_path} has {len(styles)} inline style declarations that could be extracted to CSS classes or design tokens.",
|
||||
location=Location(file_path, styles[0]['line']),
|
||||
affected_files=[file_path],
|
||||
estimated_impact=f"Reduce inline styles, improve maintainability",
|
||||
fix_suggestion="Extract repeated style properties to CSS classes or design tokens. Use className instead of style prop.",
|
||||
auto_fixable=True,
|
||||
))
|
||||
|
||||
# Create summary if many files have inline styles
|
||||
total_inline = len(inline_styles)
|
||||
if total_inline >= 10:
|
||||
wins.insert(0, QuickWin(
|
||||
type=QuickWinType.INLINE_STYLE,
|
||||
priority=QuickWinPriority.HIGH,
|
||||
title=f"Project has {total_inline} inline styles",
|
||||
description=f"Found {total_inline} inline style declarations across {len(by_file)} files. Consider migrating to CSS classes or design tokens.",
|
||||
affected_files=list(by_file.keys())[:10],
|
||||
estimated_impact=f"Improve code maintainability and bundle size",
|
||||
fix_suggestion="Run 'dss migrate inline-styles' to preview migration options.",
|
||||
auto_fixable=True,
|
||||
))
|
||||
|
||||
return wins
|
||||
|
||||
async def _find_duplicate_value_wins(self) -> List[QuickWin]:
|
||||
"""Find duplicate values that should be tokens."""
|
||||
wins = []
|
||||
|
||||
analysis = await self.style_analyzer.analyze()
|
||||
duplicates = analysis.get('duplicates', [])
|
||||
|
||||
# Find high-occurrence duplicates
|
||||
for dup in duplicates[:10]: # Top 10 duplicates
|
||||
if dup['count'] >= 5: # Only if used 5+ times
|
||||
priority = QuickWinPriority.HIGH if dup['count'] >= 10 else QuickWinPriority.MEDIUM
|
||||
|
||||
wins.append(QuickWin(
|
||||
type=QuickWinType.DUPLICATE_VALUE,
|
||||
priority=priority,
|
||||
title=f"Duplicate value '{dup['value']}' used {dup['count']} times",
|
||||
description=f"The value '{dup['value']}' appears {dup['count']} times across {len(dup['files'])} files. This should be a design token.",
|
||||
affected_files=dup['files'],
|
||||
estimated_impact=f"Create single source of truth, easier theme updates",
|
||||
fix_suggestion=f"Create token for this value and replace all occurrences.",
|
||||
auto_fixable=True,
|
||||
))
|
||||
|
||||
return wins
|
||||
|
||||
async def _find_unused_style_wins(self) -> List[QuickWin]:
|
||||
"""Find unused CSS styles."""
|
||||
wins = []
|
||||
|
||||
unused = await self.style_analyzer.find_unused_styles()
|
||||
|
||||
if len(unused) >= 5:
|
||||
wins.append(QuickWin(
|
||||
type=QuickWinType.UNUSED_STYLE,
|
||||
priority=QuickWinPriority.MEDIUM,
|
||||
title=f"Found {len(unused)} potentially unused CSS classes",
|
||||
description=f"These CSS classes are defined but don't appear to be used in the codebase. Review and remove if confirmed unused.",
|
||||
affected_files=list(set(u['file'] for u in unused))[:10],
|
||||
estimated_impact=f"Reduce CSS bundle size by removing dead code",
|
||||
fix_suggestion="Review each class and remove if unused. Some may be dynamically generated.",
|
||||
auto_fixable=False, # Needs human review
|
||||
))
|
||||
|
||||
return wins
|
||||
|
||||
async def _find_hardcoded_value_wins(self) -> List[QuickWin]:
|
||||
"""Find hardcoded magic values."""
|
||||
wins = []
|
||||
|
||||
analysis = await self.style_analyzer.analyze()
|
||||
candidates = analysis.get('token_candidates', [])
|
||||
|
||||
# Find high-confidence candidates
|
||||
high_confidence = [c for c in candidates if c.confidence >= 0.7]
|
||||
|
||||
if high_confidence:
|
||||
wins.append(QuickWin(
|
||||
type=QuickWinType.HARDCODED_VALUE,
|
||||
priority=QuickWinPriority.MEDIUM,
|
||||
title=f"Found {len(high_confidence)} values that should be tokens",
|
||||
description="These hardcoded values appear multiple times and should be extracted as design tokens for consistency.",
|
||||
estimated_impact="Improve theme consistency and make updates easier",
|
||||
fix_suggestion="Use 'dss extract-tokens' to create tokens from these values.",
|
||||
auto_fixable=True,
|
||||
))
|
||||
|
||||
# Add specific wins for top candidates
|
||||
for candidate in high_confidence[:5]:
|
||||
wins.append(QuickWin(
|
||||
type=QuickWinType.HARDCODED_VALUE,
|
||||
priority=QuickWinPriority.LOW,
|
||||
title=f"Extract '{candidate.value}' as token",
|
||||
description=f"Value '{candidate.value}' appears {candidate.occurrences} times. Suggested token: {candidate.suggested_name}",
|
||||
location=candidate.locations[0] if candidate.locations else None,
|
||||
affected_files=[loc.file_path for loc in candidate.locations[:5]],
|
||||
estimated_impact=f"Single source of truth for this value",
|
||||
fix_suggestion=f"Create token '{candidate.suggested_name}' with value '{candidate.value}'",
|
||||
auto_fixable=True,
|
||||
))
|
||||
|
||||
return wins
|
||||
|
||||
async def _find_naming_inconsistency_wins(self) -> List[QuickWin]:
|
||||
"""Find naming inconsistencies."""
|
||||
wins = []
|
||||
|
||||
naming = await self.style_analyzer.analyze_naming_consistency()
|
||||
|
||||
if naming.get('inconsistencies'):
|
||||
primary = naming.get('primary_pattern', 'unknown')
|
||||
inconsistent_count = len(naming['inconsistencies'])
|
||||
|
||||
wins.append(QuickWin(
|
||||
type=QuickWinType.NAMING_INCONSISTENCY,
|
||||
priority=QuickWinPriority.LOW,
|
||||
title=f"Found {inconsistent_count} naming inconsistencies",
|
||||
description=f"The project primarily uses {primary} naming, but {inconsistent_count} classes use different conventions.",
|
||||
affected_files=list(set(i['file'] for i in naming['inconsistencies']))[:10],
|
||||
estimated_impact="Improve code consistency and readability",
|
||||
fix_suggestion=f"Standardize all class names to use {primary} convention.",
|
||||
auto_fixable=True,
|
||||
))
|
||||
|
||||
return wins
|
||||
|
||||
async def _find_accessibility_wins(self) -> List[QuickWin]:
|
||||
"""Find accessibility issues."""
|
||||
wins = []
|
||||
skip_dirs = {'node_modules', '.git', 'dist', 'build'}
|
||||
|
||||
a11y_issues = []
|
||||
|
||||
for ext in ['*.jsx', '*.tsx']:
|
||||
for file_path in self.root.rglob(ext):
|
||||
if any(skip in file_path.parts for skip in skip_dirs):
|
||||
continue
|
||||
|
||||
try:
|
||||
content = file_path.read_text(encoding='utf-8', errors='ignore')
|
||||
rel_path = str(file_path.relative_to(self.root))
|
||||
|
||||
# Check for images without alt
|
||||
img_no_alt = re.findall(r'<img[^>]+(?<!alt=")[^>]*>', content)
|
||||
if img_no_alt:
|
||||
for match in img_no_alt[:3]:
|
||||
if 'alt=' not in match:
|
||||
line = content[:content.find(match)].count('\n') + 1
|
||||
a11y_issues.append({
|
||||
'type': 'img-no-alt',
|
||||
'file': rel_path,
|
||||
'line': line,
|
||||
})
|
||||
|
||||
# Check for buttons without accessible text
|
||||
icon_only_buttons = re.findall(
|
||||
r'<button[^>]*>\s*<(?:svg|Icon|img)[^>]*/?>\s*</button>',
|
||||
content,
|
||||
re.IGNORECASE
|
||||
)
|
||||
if icon_only_buttons:
|
||||
a11y_issues.append({
|
||||
'type': 'icon-button-no-label',
|
||||
'file': rel_path,
|
||||
})
|
||||
|
||||
# Check for click handlers on non-interactive elements
|
||||
div_onclick = re.findall(r'<div[^>]+onClick', content)
|
||||
if div_onclick:
|
||||
a11y_issues.append({
|
||||
'type': 'div-click-handler',
|
||||
'file': rel_path,
|
||||
'count': len(div_onclick),
|
||||
})
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Group issues by type
|
||||
if a11y_issues:
|
||||
img_issues = [i for i in a11y_issues if i['type'] == 'img-no-alt']
|
||||
if img_issues:
|
||||
wins.append(QuickWin(
|
||||
type=QuickWinType.ACCESSIBILITY,
|
||||
priority=QuickWinPriority.HIGH,
|
||||
title=f"Found {len(img_issues)} images without alt text",
|
||||
description="Images should have alt attributes for screen readers. Empty alt='' is acceptable for decorative images.",
|
||||
affected_files=list(set(i['file'] for i in img_issues))[:10],
|
||||
estimated_impact="Improve accessibility for screen reader users",
|
||||
fix_suggestion="Add descriptive alt text to images or alt='' for decorative images.",
|
||||
auto_fixable=False,
|
||||
))
|
||||
|
||||
div_issues = [i for i in a11y_issues if i['type'] == 'div-click-handler']
|
||||
if div_issues:
|
||||
wins.append(QuickWin(
|
||||
type=QuickWinType.ACCESSIBILITY,
|
||||
priority=QuickWinPriority.MEDIUM,
|
||||
title=f"Found click handlers on div elements",
|
||||
description="Using onClick on div elements makes them inaccessible to keyboard users. Use button or add proper ARIA attributes.",
|
||||
affected_files=list(set(i['file'] for i in div_issues))[:10],
|
||||
estimated_impact="Improve keyboard navigation accessibility",
|
||||
fix_suggestion="Replace <div onClick> with <button> or add role='button' and tabIndex={0}.",
|
||||
auto_fixable=True,
|
||||
))
|
||||
|
||||
return wins
|
||||
|
||||
async def get_summary(self) -> Dict[str, Any]:
|
||||
"""Get summary of all quick-wins."""
|
||||
wins = await self.find_all()
|
||||
|
||||
by_type = {}
|
||||
by_priority = {}
|
||||
|
||||
for win in wins:
|
||||
type_key = win.type.value
|
||||
priority_key = win.priority.value
|
||||
|
||||
if type_key not in by_type:
|
||||
by_type[type_key] = 0
|
||||
by_type[type_key] += 1
|
||||
|
||||
if priority_key not in by_priority:
|
||||
by_priority[priority_key] = 0
|
||||
by_priority[priority_key] += 1
|
||||
|
||||
return {
|
||||
'total': len(wins),
|
||||
'by_type': by_type,
|
||||
'by_priority': by_priority,
|
||||
'auto_fixable': len([w for w in wins if w.auto_fixable]),
|
||||
'top_wins': [w.to_dict() for w in wins[:10]],
|
||||
}
|
||||
|
||||
async def get_actionable_report(self) -> str:
|
||||
"""Generate human-readable report of quick-wins."""
|
||||
wins = await self.find_all()
|
||||
|
||||
if not wins:
|
||||
return "No quick-wins found. Your codebase looks clean!"
|
||||
|
||||
lines = [
|
||||
"QUICK-WIN OPPORTUNITIES",
|
||||
"=" * 50,
|
||||
"",
|
||||
]
|
||||
|
||||
# Group by priority
|
||||
by_priority = {
|
||||
QuickWinPriority.CRITICAL: [],
|
||||
QuickWinPriority.HIGH: [],
|
||||
QuickWinPriority.MEDIUM: [],
|
||||
QuickWinPriority.LOW: [],
|
||||
}
|
||||
|
||||
for win in wins:
|
||||
by_priority[win.priority].append(win)
|
||||
|
||||
# Report by priority
|
||||
priority_labels = {
|
||||
QuickWinPriority.CRITICAL: "CRITICAL",
|
||||
QuickWinPriority.HIGH: "HIGH PRIORITY",
|
||||
QuickWinPriority.MEDIUM: "MEDIUM PRIORITY",
|
||||
QuickWinPriority.LOW: "LOW PRIORITY",
|
||||
}
|
||||
|
||||
for priority, label in priority_labels.items():
|
||||
priority_wins = by_priority[priority]
|
||||
if not priority_wins:
|
||||
continue
|
||||
|
||||
lines.extend([
|
||||
f"\n[{label}] ({len(priority_wins)} items)",
|
||||
"-" * 40,
|
||||
])
|
||||
|
||||
for i, win in enumerate(priority_wins[:5], 1):
|
||||
lines.extend([
|
||||
f"\n{i}. {win.title}",
|
||||
f" {win.description[:100]}...",
|
||||
f" Impact: {win.estimated_impact}",
|
||||
])
|
||||
if win.auto_fixable:
|
||||
lines.append(" [Auto-fixable]")
|
||||
|
||||
if len(priority_wins) > 5:
|
||||
lines.append(f"\n ... and {len(priority_wins) - 5} more")
|
||||
|
||||
# Summary
|
||||
lines.extend([
|
||||
"",
|
||||
"=" * 50,
|
||||
"SUMMARY",
|
||||
f"Total quick-wins: {len(wins)}",
|
||||
f"Auto-fixable: {len([w for w in wins if w.auto_fixable])}",
|
||||
"",
|
||||
"Run 'dss fix --preview' to see suggested changes.",
|
||||
])
|
||||
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user