tractatus/scripts/check-color-contrast.js
TheFlow 2af47035ac refactor: remove website code and fix critical startup crashes (Phase 8)
CRITICAL FIX: Server would CRASH ON STARTUP (multiple import errors)

REMOVED (2 scripts):
1. scripts/framework-watchdog.js
   - Monitored .claude/session-state.json (OUR Claude Code setup)
   - Monitored .claude/token-checkpoints.json (OUR file structure)
   - Implementers won't have our .claude/ directory

2. scripts/init-db.js
   - Created website collections: blog_posts, media_inquiries, case_submissions
   - Created website collections: resources, moderation_queue, users, citations
   - Created website collections: translations, koha_donations
   - Next steps referenced deleted scripts (npm run seed:admin)

REWRITTEN (2 files):

src/models/index.js (29 lines → 27 lines)
- REMOVED imports: Document, BlogPost, MediaInquiry, CaseSubmission, Resource
- REMOVED imports: ModerationQueue, User (all deleted in Phase 2)
- KEPT imports: AuditLog, DeliberationSession, GovernanceLog, GovernanceRule
- KEPT imports: Precedent, Project, SessionState, VariableValue, VerificationLog
- Result: Only framework models exported

src/server.js (284 lines → 163 lines, 43% reduction)
- REMOVED: Imports to deleted middleware (csrf-protection, response-sanitization)
- REMOVED: Stripe webhook handling (/api/koha/webhook)
- REMOVED: Static file caching (for deleted public/ directory)
- REMOVED: Static file serving (public/ deleted in Phase 6)
- REMOVED: CSRF token endpoint
- REMOVED: Website homepage with "auth, documents, blog, admin" references
- REMOVED: Instruction sync (scripts/sync-instructions-to-db.js reference)
- REMOVED: Hardcoded log path (${process.env.HOME}/var/log/tractatus/...)
- REMOVED: Website-specific security middleware
- KEPT: Security headers, rate limiting, CORS, body parsers
- KEPT: API routes, governance services, MongoDB connections
- RESULT: Clean framework-only server

RESULT: Repository can now start without crashes, all imports resolve

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 22:17:02 +13:00

226 lines
6.5 KiB
JavaScript
Executable file

#!/usr/bin/env node
/**
* Color Contrast Checker
*
* Verifies color contrast ratios meet WCAG 2.1 AA standards (4.5:1 normal text, 3:1 large text)
*
* Copyright 2025 Tractatus Project
* Licensed under Apache License 2.0
*/
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function success(message) {
log(`${message}`, 'green');
}
function warning(message) {
log(`${message}`, 'yellow');
}
function error(message) {
log(`${message}`, 'red');
}
/**
* Convert hex color to RGB
*/
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
/**
* Calculate relative luminance (WCAG formula)
*/
function getLuminance(rgb) {
const rsRGB = rgb.r / 255;
const gsRGB = rgb.g / 255;
const bsRGB = rgb.b / 255;
const r = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
const g = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
const b = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
/**
* Calculate contrast ratio between two colors
*/
function getContrastRatio(color1, color2) {
const rgb1 = hexToRgb(color1);
const rgb2 = hexToRgb(color2);
const lum1 = getLuminance(rgb1);
const lum2 = getLuminance(rgb2);
const lighter = Math.max(lum1, lum2);
const darker = Math.min(lum1, lum2);
return (lighter + 0.05) / (darker + 0.05);
}
/**
* Check if contrast ratio meets WCAG AA standards
*/
function meetsWCAG_AA(ratio, largeText = false) {
const threshold = largeText ? 3.0 : 4.5;
return ratio >= threshold;
}
/**
* Tailwind color palette (common colors used in Tractatus site)
*/
const tailwindColors = {
'white': '#ffffff',
'gray-50': '#f9fafb',
'gray-100': '#f3f4f6',
'gray-200': '#e5e7eb',
'gray-300': '#d1d5db',
'gray-400': '#9ca3af',
'gray-500': '#6b7280',
'gray-600': '#4b5563',
'gray-700': '#374151',
'gray-800': '#1f2937',
'gray-900': '#111827',
'blue-50': '#eff6ff',
'blue-100': '#dbeafe',
'blue-400': '#60a5fa',
'blue-500': '#3b82f6',
'blue-600': '#2563eb',
'blue-700': '#1d4ed8',
'blue-800': '#1e40af',
'blue-900': '#1e3a8a',
'purple-500': '#a855f7',
'purple-600': '#9333ea',
'purple-700': '#7e22ce',
'green-500': '#22c55e',
'green-600': '#16a34a',
'green-700': '#15803d',
'yellow-600': '#ca8a04',
'amber-500': '#f59e0b',
'amber-800': '#92400e',
'amber-900': '#78350f',
'red-600': '#dc2626'
};
/**
* Color combinations used on site
*/
const colorCombinations = [
// Body text on backgrounds
{ name: 'Body text (gray-900 on white)', fg: 'gray-900', bg: 'white', largeText: false },
{ name: 'Body text (gray-700 on white)', fg: 'gray-700', bg: 'white', largeText: false },
{ name: 'Body text (gray-600 on white)', fg: 'gray-600', bg: 'white', largeText: false },
{ name: 'Muted text (gray-500 on white)', fg: 'gray-500', bg: 'white', largeText: false },
// Links
{ name: 'Link (blue-600 on white)', fg: 'blue-600', bg: 'white', largeText: false },
{ name: 'Link hover (blue-700 on white)', fg: 'blue-700', bg: 'white', largeText: false },
// Buttons
{ name: 'Button text (white on blue-600)', fg: 'white', bg: 'blue-600', largeText: false },
{ name: 'Button hover (white on blue-700)', fg: 'white', bg: 'blue-700', largeText: false },
{ name: 'Purple button (white on purple-600)', fg: 'white', bg: 'purple-600', largeText: false },
{ name: 'Green button (white on green-700)', fg: 'white', bg: 'green-700', largeText: false },
// Hero section
{ name: 'Hero subtitle (blue-100 on blue-700)', fg: 'blue-100', bg: 'blue-700', largeText: true },
// Footer
{ name: 'Footer text (gray-400 on gray-900)', fg: 'gray-400', bg: 'gray-900', largeText: false },
{ name: 'Footer links (blue-400 on gray-900)', fg: 'blue-400', bg: 'gray-900', largeText: false },
// Alerts/Messages
{ name: 'Success message (green-900 on green-50)', fg: '#065f46', bg: '#d1fae5', largeText: false },
{ name: 'Error message (red-900 on red-50)', fg: '#991b1b', bg: '#fee2e2', largeText: false },
{ name: 'Warning message (amber-900 on amber-50)', fg: 'amber-900', bg: '#fef3c7', largeText: false },
// Cards/Sections
{ name: 'Card text (gray-700 on white)', fg: 'gray-700', bg: 'white', largeText: false },
{ name: 'Card header (gray-900 on white)', fg: 'gray-900', bg: 'white', largeText: true },
];
/**
* Main check
*/
function main() {
log('═'.repeat(70), 'cyan');
log(' Color Contrast Checker (WCAG 2.1 AA)', 'bright');
log('═'.repeat(70), 'cyan');
console.log('');
let passCount = 0;
let failCount = 0;
let warnings = 0;
colorCombinations.forEach(combo => {
const fgColor = tailwindColors[combo.fg] || combo.fg;
const bgColor = tailwindColors[combo.bg] || combo.bg;
const ratio = getContrastRatio(fgColor, bgColor);
const passes = meetsWCAG_AA(ratio, combo.largeText);
const threshold = combo.largeText ? '3:1' : '4.5:1';
const ratioStr = ratio.toFixed(2) + ':1';
if (passes) {
success(`${combo.name.padEnd(45)} ${ratioStr.padStart(8)} (>= ${threshold}) ✓`);
passCount++;
} else {
// Check if it's close (within 0.3 of threshold)
const minRatio = combo.largeText ? 3.0 : 4.5;
if (ratio >= minRatio - 0.3) {
warning(`${combo.name.padEnd(45)} ${ratioStr.padStart(8)} (< ${threshold}) ⚠`);
warnings++;
} else {
error(`${combo.name.padEnd(45)} ${ratioStr.padStart(8)} (< ${threshold}) ✗`);
failCount++;
}
}
});
console.log('');
log('═'.repeat(70), 'cyan');
log(' Summary', 'bright');
log('═'.repeat(70), 'cyan');
console.log('');
log(` Combinations Checked: ${colorCombinations.length}`, 'bright');
log(` Passed: ${passCount}`, 'green');
if (warnings > 0) log(` Warnings: ${warnings}`, 'yellow');
if (failCount > 0) log(` Failed: ${failCount}`, 'red');
console.log('');
if (failCount > 0) {
error('Some color combinations fail WCAG AA standards');
console.log('');
process.exit(1);
} else if (warnings > 0) {
warning('All combinations pass, but some are borderline');
console.log('');
process.exit(0);
} else {
success('All color combinations meet WCAG AA standards');
console.log('');
process.exit(0);
}
}
main();