ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
471 lines • 20 kB
JavaScript
export class AuditEngine {
async performAudit(page, categories = ['all']) {
const results = {};
if (categories.includes('all') || categories.includes('accessibility')) {
results.accessibility = await this.auditAccessibility(page);
}
if (categories.includes('all') || categories.includes('performance')) {
results.performance = await this.auditPerformance(page);
}
if (categories.includes('all') || categories.includes('seo')) {
results.seo = await this.auditSEO(page);
}
if (categories.includes('all') || categories.includes('bestPractices')) {
results.bestPractices = await this.auditBestPractices(page);
}
if (categories.includes('all') || categories.includes('security')) {
results.security = await this.auditSecurity(page);
}
return results;
}
async auditAccessibility(page) {
const issues = [];
const suggestions = [];
const audit = await page.evaluate(() => {
const results = {
imagesWithoutAlt: 0,
buttonsWithoutText: 0,
inputsWithoutLabels: 0,
lowContrastElements: 0,
missingLandmarks: false,
formIssues: 0,
};
// Check images without alt text
document.querySelectorAll('img:not([alt])').forEach((img) => {
results.imagesWithoutAlt++;
});
// Check buttons without accessible text
document.querySelectorAll('button').forEach((button) => {
if (!button.textContent?.trim() && !button.getAttribute('aria-label')) {
results.buttonsWithoutText++;
}
});
// Check inputs without labels
document.querySelectorAll('input:not([type="hidden"])').forEach((input) => {
const id = input.id;
if (!id || !document.querySelector(`label[for="${id}"]`)) {
if (!input.getAttribute('aria-label')) {
results.inputsWithoutLabels++;
}
}
});
// Check for main landmark
if (!document.querySelector('main, [role="main"]')) {
results.missingLandmarks = true;
}
// Check form accessibility
document.querySelectorAll('form').forEach((form) => {
if (!form.querySelector('button[type="submit"], input[type="submit"]')) {
results.formIssues++;
}
});
return results;
});
// Calculate score (0-100)
let score = 100;
if (audit.imagesWithoutAlt > 0) {
score -= Math.min(20, audit.imagesWithoutAlt * 5);
issues.push({
severity: 'error',
message: `${audit.imagesWithoutAlt} images missing alt text`,
details: { count: audit.imagesWithoutAlt }
});
suggestions.push('Add descriptive alt text to all images');
}
if (audit.buttonsWithoutText > 0) {
score -= Math.min(15, audit.buttonsWithoutText * 5);
issues.push({
severity: 'error',
message: `${audit.buttonsWithoutText} buttons without accessible text`,
details: { count: audit.buttonsWithoutText }
});
suggestions.push('Add text content or aria-label to buttons');
}
if (audit.inputsWithoutLabels > 0) {
score -= Math.min(15, audit.inputsWithoutLabels * 3);
issues.push({
severity: 'error',
message: `${audit.inputsWithoutLabels} form inputs without labels`,
details: { count: audit.inputsWithoutLabels }
});
suggestions.push('Associate labels with form inputs using "for" attribute');
}
if (audit.missingLandmarks) {
score -= 10;
issues.push({
severity: 'warning',
message: 'Missing main landmark',
details: { landmark: 'main' }
});
suggestions.push('Add <main> element or role="main" to primary content');
}
return {
category: 'accessibility',
score: Math.max(0, score),
issues,
suggestions
};
}
async auditPerformance(page) {
const issues = [];
const suggestions = [];
// Get performance metrics
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadTime: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
resources: performance.getEntriesByType('resource').length,
totalSize: performance.getEntriesByType('resource').reduce((acc, r) => acc + (r.transferSize || 0), 0),
largeImages: Array.from(document.querySelectorAll('img')).filter((img) => {
return img.naturalWidth > 1920 || img.naturalHeight > 1080;
}).length,
scripts: document.querySelectorAll('script').length,
stylesheets: document.querySelectorAll('link[rel="stylesheet"]').length,
};
});
// Calculate score
let score = 100;
// Check load time
if (metrics.loadTime > 3000) {
score -= 20;
issues.push({
severity: 'warning',
message: `Page load time is ${(metrics.loadTime / 1000).toFixed(2)}s (target: <3s)`,
details: { loadTime: metrics.loadTime }
});
suggestions.push('Optimize resource loading and reduce page weight');
}
// Check FCP
if (metrics.firstContentfulPaint > 1800) {
score -= 15;
issues.push({
severity: 'warning',
message: `First Contentful Paint is ${(metrics.firstContentfulPaint / 1000).toFixed(2)}s (target: <1.8s)`,
details: { fcp: metrics.firstContentfulPaint }
});
suggestions.push('Reduce render-blocking resources');
}
// Check resource count
if (metrics.resources > 50) {
score -= 10;
issues.push({
severity: 'info',
message: `Loading ${metrics.resources} resources (consider bundling)`,
details: { resourceCount: metrics.resources }
});
suggestions.push('Bundle and minify resources');
}
// Check for large images
if (metrics.largeImages > 0) {
score -= 10;
issues.push({
severity: 'warning',
message: `${metrics.largeImages} oversized images detected`,
details: { largeImages: metrics.largeImages }
});
suggestions.push('Optimize images - use appropriate sizes and formats (WebP, AVIF)');
}
// Check total size
const totalSizeMB = metrics.totalSize / (1024 * 1024);
if (totalSizeMB > 5) {
score -= 15;
issues.push({
severity: 'warning',
message: `Total page size is ${totalSizeMB.toFixed(2)}MB (target: <5MB)`,
details: { totalSize: metrics.totalSize }
});
suggestions.push('Reduce page weight through compression and optimization');
}
return {
category: 'performance',
score: Math.max(0, score),
issues,
suggestions
};
}
async auditSEO(page) {
const issues = [];
const suggestions = [];
const seoData = await page.evaluate(() => {
const title = document.querySelector('title')?.textContent || '';
const metaDescription = document.querySelector('meta[name="description"]')?.getAttribute('content') || '';
const h1Count = document.querySelectorAll('h1').length;
const canonicalLink = document.querySelector('link[rel="canonical"]')?.getAttribute('href');
const viewport = document.querySelector('meta[name="viewport"]')?.getAttribute('content');
const ogTags = {
title: document.querySelector('meta[property="og:title"]')?.getAttribute('content'),
description: document.querySelector('meta[property="og:description"]')?.getAttribute('content'),
image: document.querySelector('meta[property="og:image"]')?.getAttribute('content'),
};
return {
title,
titleLength: title.length,
metaDescription,
descriptionLength: metaDescription.length,
h1Count,
hasCanonical: !!canonicalLink,
hasViewport: !!viewport,
hasOgTags: !!(ogTags.title && ogTags.description),
hasRobotsTxt: false, // Would need server check
};
});
let score = 100;
// Check title
if (!seoData.title) {
score -= 20;
issues.push({
severity: 'error',
message: 'Missing page title',
details: { field: 'title' }
});
suggestions.push('Add a descriptive <title> tag');
}
else if (seoData.titleLength < 30 || seoData.titleLength > 60) {
score -= 10;
issues.push({
severity: 'warning',
message: `Title length is ${seoData.titleLength} characters (ideal: 30-60)`,
details: { titleLength: seoData.titleLength }
});
suggestions.push('Optimize title length for search results');
}
// Check meta description
if (!seoData.metaDescription) {
score -= 15;
issues.push({
severity: 'error',
message: 'Missing meta description',
details: { field: 'description' }
});
suggestions.push('Add a compelling meta description');
}
else if (seoData.descriptionLength < 120 || seoData.descriptionLength > 160) {
score -= 5;
issues.push({
severity: 'warning',
message: `Meta description length is ${seoData.descriptionLength} characters (ideal: 120-160)`,
details: { descriptionLength: seoData.descriptionLength }
});
}
// Check H1
if (seoData.h1Count === 0) {
score -= 15;
issues.push({
severity: 'error',
message: 'No H1 tag found',
details: { h1Count: 0 }
});
suggestions.push('Add a single H1 tag with primary keyword');
}
else if (seoData.h1Count > 1) {
score -= 5;
issues.push({
severity: 'warning',
message: `Multiple H1 tags found (${seoData.h1Count})`,
details: { h1Count: seoData.h1Count }
});
suggestions.push('Use only one H1 tag per page');
}
// Check mobile viewport
if (!seoData.hasViewport) {
score -= 15;
issues.push({
severity: 'error',
message: 'Missing viewport meta tag',
details: { field: 'viewport' }
});
suggestions.push('Add <meta name="viewport" content="width=device-width, initial-scale=1">');
}
// Check Open Graph
if (!seoData.hasOgTags) {
score -= 5;
issues.push({
severity: 'info',
message: 'Missing Open Graph tags for social sharing',
details: { field: 'og:tags' }
});
suggestions.push('Add Open Graph meta tags for better social media sharing');
}
return {
category: 'seo',
score: Math.max(0, score),
issues,
suggestions
};
}
async auditBestPractices(page) {
const issues = [];
const suggestions = [];
const practices = await page.evaluate(() => {
const hasHttps = window.location.protocol === 'https:';
const hasLang = !!document.documentElement.getAttribute('lang');
const hasCharset = !!document.querySelector('meta[charset]');
const consoleErrors = window.__AI_DEBUG__?.events?.filter((e) => e.type === 'console.error').length || 0;
const hasFavicon = !!document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
// Check for common bad practices
const inlineStyles = document.querySelectorAll('[style]').length;
const inlineScripts = document.querySelectorAll('script:not([src])').length;
return {
hasHttps,
hasLang,
hasCharset,
consoleErrors,
hasFavicon,
inlineStyles,
inlineScripts,
};
});
let score = 100;
if (!practices.hasLang) {
score -= 10;
issues.push({
severity: 'warning',
message: 'Missing lang attribute on html element',
details: { field: 'lang' }
});
suggestions.push('Add lang attribute to <html> element');
}
if (!practices.hasCharset) {
score -= 10;
issues.push({
severity: 'warning',
message: 'Missing charset meta tag',
details: { field: 'charset' }
});
suggestions.push('Add <meta charset="UTF-8">');
}
if (practices.consoleErrors > 0) {
score -= Math.min(20, practices.consoleErrors * 5);
issues.push({
severity: 'error',
message: `${practices.consoleErrors} console errors detected`,
details: { errorCount: practices.consoleErrors }
});
suggestions.push('Fix JavaScript errors in console');
}
if (!practices.hasFavicon) {
score -= 5;
issues.push({
severity: 'info',
message: 'Missing favicon',
details: { field: 'favicon' }
});
suggestions.push('Add a favicon for better branding');
}
if (practices.inlineStyles > 10) {
score -= 10;
issues.push({
severity: 'warning',
message: `${practices.inlineStyles} inline styles found`,
details: { inlineStyles: practices.inlineStyles }
});
suggestions.push('Move inline styles to CSS files');
}
if (practices.inlineScripts > 2) {
score -= 10;
issues.push({
severity: 'warning',
message: `${practices.inlineScripts} inline scripts found`,
details: { inlineScripts: practices.inlineScripts }
});
suggestions.push('Move inline scripts to external files');
}
return {
category: 'bestPractices',
score: Math.max(0, score),
issues,
suggestions
};
}
async auditSecurity(page) {
const issues = [];
const suggestions = [];
const security = await page.evaluate(() => {
const forms = document.querySelectorAll('form');
const passwordInputs = document.querySelectorAll('input[type="password"]');
const httpLinks = Array.from(document.querySelectorAll('a[href^="http://"]')).length;
const externalScripts = Array.from(document.querySelectorAll('script[src]'))
.filter(s => {
const src = s.getAttribute('src') || '';
return src.startsWith('http') && !src.includes(window.location.hostname);
}).length;
// Check for sensitive data in URLs
const urlParams = new URLSearchParams(window.location.search);
const sensitiveParams = ['password', 'token', 'key', 'secret', 'auth'].filter(param => urlParams.has(param));
return {
formCount: forms.length,
httpsFormActions: Array.from(forms).filter(f => {
const action = f.getAttribute('action') || '';
return !action || action.startsWith('https://') || action.startsWith('/');
}).length,
passwordInputs: passwordInputs.length,
autocompleteOffPasswords: Array.from(passwordInputs).filter(p => p.getAttribute('autocomplete') === 'off' || p.getAttribute('autocomplete') === 'new-password').length,
httpLinks,
externalScripts,
sensitiveParams,
};
});
let score = 100;
// Check forms use HTTPS
if (security.formCount > security.httpsFormActions) {
score -= 20;
issues.push({
severity: 'error',
message: 'Forms submitting over insecure HTTP',
details: { insecureForms: security.formCount - security.httpsFormActions }
});
suggestions.push('Ensure all forms submit over HTTPS');
}
// Check password autocomplete
if (security.passwordInputs > security.autocompleteOffPasswords) {
score -= 10;
issues.push({
severity: 'warning',
message: 'Password fields allow autocomplete',
details: { count: security.passwordInputs - security.autocompleteOffPasswords }
});
suggestions.push('Set autocomplete="new-password" on password fields');
}
// Check for HTTP links
if (security.httpLinks > 0) {
score -= 15;
issues.push({
severity: 'warning',
message: `${security.httpLinks} links use insecure HTTP`,
details: { httpLinks: security.httpLinks }
});
suggestions.push('Update all links to use HTTPS');
}
// Check external scripts
if (security.externalScripts > 5) {
score -= 10;
issues.push({
severity: 'info',
message: `${security.externalScripts} external scripts loaded`,
details: { externalScripts: security.externalScripts }
});
suggestions.push('Audit external scripts and consider using Subresource Integrity (SRI)');
}
// Check for sensitive data in URL
if (security.sensitiveParams.length > 0) {
score -= 25;
issues.push({
severity: 'error',
message: `Sensitive data in URL parameters: ${security.sensitiveParams.join(', ')}`,
details: { params: security.sensitiveParams }
});
suggestions.push('Never pass sensitive data in URL parameters');
}
return {
category: 'security',
score: Math.max(0, score),
issues,
suggestions
};
}
}
//# sourceMappingURL=audit-engine.js.map