#!/usr/bin/env node /* Token contract guard — runs as part of `npm run check`. Bans hardcoded values in application CSS so every visual change goes through src/tokens.css. */ import { readFileSync, readdirSync, statSync } from 'node:fs' import { join } from 'node:path' const CSS_ROOTS = ['src'] const EXEMPT = new Set(['tokens.css']) // Neutral shine/shadow literals stay allowed (they are surfaces, not palette). const RGB_ALLOW = [/rgba\(\s*0\s*,\s*0\s*,\s*0/, /rgba\(\s*255\s*,\s*255\s*,\s*255/] const violations = [] function walk(dir) { for (const entry of readdirSync(dir)) { const full = join(dir, entry) if (statSync(full).isDirectory()) { walk(full) } else if (full.endsWith('.css') && !EXEMPT.has(entry)) { checkFile(full, readFileSync(full, 'utf8')) } } } function checkFile(file, css) { css.split('\n').forEach((line, i) => { const n = `${file}:${i + 1}` const hex = line.match(/#[0-9a-fA-F]{3,8}\b/) if (hex) violations.push(`${n} hex color ${hex[0]} — add a token in tokens.css instead`) if (/font(-size)?\s*:[^;}]*\d+px/.test(line)) violations.push(`${n} px font size — use var(--text-*)`) const rgb = line.match(/rgba?\(/) if (rgb && !RGB_ALLOW.some(re => re.test(line))) { violations.push(`${n} raw rgba() — use color-mix(in srgb, var(--token) N%, transparent)`) } }) } CSS_ROOTS.forEach(walk) if (violations.length) { console.error(`\n✗ token contract violations:\n\n${violations.join('\n')}\n`) process.exit(1) } else { console.log('✓ token contract OK — no hardcoded colors or font sizes found') }