const audit = {
score: 0,
dimensions: {},
issues: [],
passed: [],
critical_count: 0
}
// ═══════════════════════════════════════════
// Dimension 1: Code Quality (weight: 0.20)
// ═══════════════════════════════════════════
const codeQuality = { score: 10, issues: [] }
for (const [file, content] of Object.entries(fileContents)) {
// Check: consistent naming conventions
// Check: no unused imports/variables
// Check: reasonable file length (< 300 lines)
if (content.split('\n').length > 300) {
codeQuality.issues.push({ file, severity: 'MEDIUM', message: 'File exceeds 300 lines, consider splitting' })
codeQuality.score -= 1
}
// Check: no console.log in production code
if (/console\.(log|debug)/.test(content) && !/\.test\.|\.spec\./.test(file)) {
codeQuality.issues.push({ file, severity: 'LOW', message: 'console.log found in production code' })
codeQuality.score -= 0.5
}
// Check: proper error handling
if (/catch\s*\(\s*\)\s*\{[\s]*\}/.test(content)) {
codeQuality.issues.push({ file, severity: 'HIGH', message: 'Empty catch block found' })
codeQuality.score -= 2
}
}
audit.dimensions.code_quality = { weight: 0.20, score: Math.max(0, codeQuality.score), issues: codeQuality.issues }
// ═══════════════════════════════════════════
// Dimension 2: Accessibility (weight: 0.25)
// ═══════════════════════════════════════════
const accessibility = { score: 10, issues: [] }
for (const [file, content] of Object.entries(fileContents)) {
if (!/\.(tsx|jsx|vue|svelte|html)$/.test(file)) continue
// Check: images have alt text
if (/<img\s/.test(content) && !/<img\s[^>]*alt=/.test(content)) {
accessibility.issues.push({ file, severity: 'CRITICAL', message: 'Image missing alt attribute', do: 'Always provide alt text', dont: 'Leave alt empty for decorative images without role="presentation"' })
accessibility.score -= 3
}
// Check: form inputs have labels
if (/<input\s/.test(content) && !/<label/.test(content) && !/aria-label/.test(content)) {
accessibility.issues.push({ file, severity: 'HIGH', message: 'Form input missing associated label', do: 'Use <label> or aria-label', dont: 'Rely on placeholder as label' })
accessibility.score -= 2
}
// Check: buttons have accessible text
if (/<button\s/.test(content) && /<button\s[^>]*>\s*</.test(content) && !/aria-label/.test(content)) {
accessibility.issues.push({ file, severity: 'HIGH', message: 'Button may lack accessible text (icon-only?)', do: 'Add aria-label for icon-only buttons', dont: 'Use title attribute as sole accessible name' })
accessibility.score -= 2
}
// Check: heading hierarchy
if (/h[1-6]/.test(content)) {
const headings = content.match(/<h([1-6])/g)?.map(h => parseInt(h[2])) || []
for (let i = 1; i < headings.length; i++) {
if (headings[i] - headings[i-1] > 1) {
accessibility.issues.push({ file, severity: 'MEDIUM', message: `Heading level skipped: h${headings[i-1]} → h${headings[i]}` })
accessibility.score -= 1
}
}
}
// Check: color contrast (basic — flag hardcoded light colors on light bg)
// Check: focus-visible styles
if (/button|<a |input|select/.test(content) && !/focus-visible|focus:/.test(content)) {
accessibility.issues.push({ file, severity: 'HIGH', message: 'Interactive element missing focus styles', do: 'Add focus-visible outline', dont: 'Remove default focus outline without replacement' })
accessibility.score -= 2
}
// Check: ARIA roles used correctly
if (/role=/.test(content) && /role="(button|link)"/.test(content)) {
// Verify tabindex is present for non-native elements with role
if (!/tabindex/.test(content)) {
accessibility.issues.push({ file, severity: 'MEDIUM', message: 'Element with ARIA role may need tabindex' })
accessibility.score -= 1
}
}
}
// Strict mode: additional checks for medical/financial
if (strictness === 'strict') {
for (const [file, content] of Object.entries(fileContents)) {
// Check: prefers-reduced-motion
if (/animation|transition|@keyframes/.test(content) && !/prefers-reduced-motion/.test(content)) {
accessibility.issues.push({ file, severity: 'HIGH', message: 'Animation without prefers-reduced-motion respect', do: 'Wrap animations in @media (prefers-reduced-motion: no-preference)', dont: 'Force animations on all users' })
accessibility.score -= 2
}
}
}
audit.dimensions.accessibility = { weight: 0.25, score: Math.max(0, accessibility.score), issues: accessibility.issues }
// ═══════════════════════════════════════════
// Dimension 3: Design Compliance (weight: 0.20)
// ═══════════════════════════════════════════
const designCompliance = { score: 10, issues: [] }
for (const [file, content] of Object.entries(fileContents)) {
// Check: using design tokens (no hardcoded colors)
if (file !== 'src/styles/tokens.css' && /#[0-9a-fA-F]{3,8}/.test(content)) {
const hardcodedColors = content.match(/#[0-9a-fA-F]{3,8}/g) || []
designCompliance.issues.push({ file, severity: 'HIGH', message: `${hardcodedColors.length} hardcoded color(s) found — use design token variables`, do: 'Use var(--color-primary)', dont: 'Hardcode #1976d2' })
designCompliance.score -= 2
}
// Check: using spacing tokens
if (/margin|padding/.test(content) && /:\s*\d+px/.test(content) && !/var\(--space/.test(content)) {
designCompliance.issues.push({ file, severity: 'MEDIUM', message: 'Hardcoded spacing values — use spacing tokens', do: 'Use var(--space-md)', dont: 'Hardcode 16px' })
designCompliance.score -= 1
}
// Check: industry anti-patterns
for (const pattern of antiPatterns) {
// Each anti-pattern is a string description — check for common violations
if (typeof pattern === 'string') {
const patternLower = pattern.toLowerCase()
if (patternLower.includes('gradient') && /gradient/.test(content)) {
designCompliance.issues.push({ file, severity: 'CRITICAL', message: `Industry anti-pattern violation: ${pattern}` })
designCompliance.score -= 3
}
if (patternLower.includes('emoji') && /[\u{1F300}-\u{1F9FF}]/u.test(content)) {
designCompliance.issues.push({ file, severity: 'HIGH', message: `Industry anti-pattern violation: ${pattern}` })
designCompliance.score -= 2
}
}
}
}
audit.dimensions.design_compliance = { weight: 0.20, score: Math.max(0, designCompliance.score), issues: designCompliance.issues }
// ═══════════════════════════════════════════
// Dimension 4: UX Best Practices (weight: 0.20)
// ═══════════════════════════════════════════
const uxPractices = { score: 10, issues: [] }
for (const [file, content] of Object.entries(fileContents)) {
// Check: cursor-pointer on clickable elements
if (/button|<a |onClick|@click/.test(content) && !/cursor-pointer/.test(content) && /\.css$/.test(file)) {
uxPractices.issues.push({ file, severity: 'MEDIUM', message: 'Missing cursor: pointer on clickable element', do: 'Add cursor: pointer to all clickable elements', dont: 'Leave default cursor on buttons/links' })
uxPractices.score -= 1
}
// Check: transition duration in valid range (150-300ms)
const durations = content.match(/duration[:-]\s*(\d+)/g) || []
for (const d of durations) {
const ms = parseInt(d.match(/\d+/)[0])
if (ms > 0 && (ms < 100 || ms > 500)) {
uxPractices.issues.push({ file, severity: 'LOW', message: `Transition duration ${ms}ms outside recommended range (150-300ms)` })
uxPractices.score -= 0.5
}
}
// Check: responsive breakpoints
if (/className|class=/.test(content) && !/md:|lg:|@media/.test(content) && /\.(tsx|jsx|vue|html)$/.test(file)) {
uxPractices.issues.push({ file, severity: 'MEDIUM', message: 'No responsive breakpoints detected', do: 'Use mobile-first responsive design', dont: 'Design for desktop only' })
uxPractices.score -= 1
}
// Check: loading states for async operations
if (/fetch|axios|useSWR|useQuery/.test(content) && !/loading|isLoading|skeleton|spinner/.test(content)) {
uxPractices.issues.push({ file, severity: 'MEDIUM', message: 'Async operation without loading state', do: 'Show loading indicator during data fetching', dont: 'Leave blank screen while loading' })
uxPractices.score -= 1
}
// Check: error states
if (/fetch|axios|useSWR|useQuery/.test(content) && !/error|isError|catch/.test(content)) {
uxPractices.issues.push({ file, severity: 'HIGH', message: 'Async operation without error handling', do: 'Show user-friendly error message', dont: 'Silently fail or show raw error' })
uxPractices.score -= 2
}
}
audit.dimensions.ux_practices = { weight: 0.20, score: Math.max(0, uxPractices.score), issues: uxPractices.issues }
// ═══════════════════════════════════════════
// Dimension 5: Pre-Delivery Checklist (weight: 0.15)
// ═══════════════════════════════════════════
const preDelivery = { score: 10, issues: [] }
// Only run full pre-delivery on final review
if (reviewType === 'final' || reviewType === 'code-review') {
const allContent = Object.values(fileContents).join('\n')
const checklist = [
{ check: "No emojis as functional icons", test: () => /[\u{1F300}-\u{1F9FF}]/u.test(allContent), severity: 'HIGH' },
{ check: "cursor-pointer on clickable", test: () => /button|onClick/.test(allContent) && !/cursor-pointer/.test(allContent), severity: 'MEDIUM' },
{ check: "Transitions 150-300ms", test: () => { const m = allContent.match(/duration[:-]\s*(\d+)/g); return m?.some(d => { const v = parseInt(d.match(/\d+/)[0]); return v > 0 && (v < 100 || v > 500) }) }, severity: 'LOW' },
{ check: "Focus states visible", test: () => /button|input|<a /.test(allContent) && !/focus/.test(allContent), severity: 'HIGH' },
{ check: "prefers-reduced-motion", test: () => /animation|@keyframes/.test(allContent) && !/prefers-reduced-motion/.test(allContent), severity: 'MEDIUM' },
{ check: "Responsive breakpoints", test: () => !/md:|lg:|@media.*min-width/.test(allContent), severity: 'MEDIUM' },
{ check: "No hardcoded colors", test: () => { const nonToken = Object.entries(fileContents).filter(([f]) => f !== 'src/styles/tokens.css'); return nonToken.some(([,c]) => /#[0-9a-fA-F]{6}/.test(c)) }, severity: 'HIGH' },
{ check: "Dark mode support", test: () => !/prefers-color-scheme|dark:|\.dark/.test(allContent), severity: 'MEDIUM' }
]
for (const item of checklist) {
try {
if (item.test()) {
preDelivery.issues.push({ check: item.check, severity: item.severity, message: `Pre-delivery check failed: ${item.check}` })
preDelivery.score -= (item.severity === 'HIGH' ? 2 : item.severity === 'MEDIUM' ? 1 : 0.5)
}
} catch {}
}
}
audit.dimensions.pre_delivery = { weight: 0.15, score: Math.max(0, preDelivery.score), issues: preDelivery.issues }