#!/usr/bin/env node /* * Rendered visual contract for AI observability pages. * * This complements the static template guard by checking computed CSS in a * browser: typography, warm-token surfaces, radius, hero backgrounds, and * mobile density across the 10 observability pages. */ const fs = require('fs'); const os = require('os'); const path = require('path'); const Module = require('module'); const DEFAULT_BASE_URL = 'https://mo.wooo.work'; const ROUTES = [ '/observability/overview', '/observability/agent_orchestration', '/observability/business_intel', '/observability/host_health', '/observability/ai_calls', '/observability/budget', '/observability/promotion_review', '/observability/rag_queries', '/observability/quality_trend', '/observability/ppt_audit_history', ]; const VIEWPORTS = [ { name: 'desktop-1440', width: 1440, height: 950, maxHeroHeight: 430, maxTitleSize: 30 }, { name: 'tablet-630', width: 630, height: 968, maxHeroHeight: 520, maxTitleSize: 30 }, { name: 'mobile-390', width: 390, height: 844, maxHeroHeight: 560, maxTitleSize: 26 }, ]; const HERO_SELECTORS = [ '.obs-hero', '.agent-hero', '.biz-command', '.runtime-hero', '.calls-hero', '.gov-hero', '.gate-hero', '.rag-hero', '.qa-hero', '.quality-hero', '.ppt-hero', ].join(','); const TITLE_SELECTORS = [ '.obs-title', '.agent-title', '.biz-title', '.runtime-title', '.calls-title', '.gov-title', '.gate-title', '.rag-title', '.qa-title', '.quality-title', '.ppt-title', '.container-fluid > h2:first-child', ].join(','); const SURFACE_SELECTORS = [ HERO_SELECTORS, '.obs-panel', '.agent-panel', '.biz-panel', '.runtime-panel', '.calls-panel', '.gov-panel', '.gate-panel', '.rag-panel', '.qa-panel', '.quality-panel', '.ppt-panel', '.obs-signal', '.agent-signal', '.biz-signal', '.runtime-signal', '.calls-signal', '.gov-signal', '.gate-signal', '.rag-signal', '.qa-signal', '.quality-signal', '.ppt-signal', '.biz-filter-card', '.biz-strategy-card', '.episode-card', '.host-lane', '.caller-card', '.agent-card', '.rec-card', '.fix-card', '.root-card', '.strategy-card', ].join(','); function parseArgs(argv) { const options = { baseUrl: DEFAULT_BASE_URL, routes: [], json: false, timeoutMs: 30000 }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === '--base-url') { options.baseUrl = argv[++i]; } else if (arg === '--route') { options.routes.push(argv[++i]); } else if (arg === '--routes') { options.routes.push(...argv[++i].split(',').map((item) => item.trim()).filter(Boolean)); } else if (arg === '--timeout') { options.timeoutMs = parseInt(argv[++i], 10) * 1000; } else if (arg === '--json') { options.json = true; } else if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); } else { throw new Error(`Unknown argument: ${arg}`); } } if (options.routes.length === 0) { options.routes = ROUTES; } return options; } function printHelp() { console.log(`AI observability rendered visual contract Options: --base-url URL Base URL, default ${DEFAULT_BASE_URL} --route PATH Add one route, can be repeated --routes A,B,C Add comma-separated routes --timeout SEC Navigation timeout, default 30 --json Print JSON results `); } function requirePlaywright() { try { return require('playwright'); } catch (error) { const candidates = [ process.env.PLAYWRIGHT_NODE_MODULE_DIR, process.env.NODE_PATH, path.join(os.homedir(), '.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules'), ].filter(Boolean); for (const candidate of candidates) { const packageJson = path.join(candidate, 'playwright', 'package.json'); if (fs.existsSync(packageJson)) { return Module.createRequire(packageJson)('playwright'); } } throw new Error('Cannot load playwright. Set NODE_PATH or PLAYWRIGHT_NODE_MODULE_DIR to a node_modules directory containing playwright.'); } } function findChromeExecutable() { const candidates = [ process.env.RESPONSIVE_CHROME_PATH, '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', ].filter(Boolean); return candidates.find((candidate) => fs.existsSync(candidate)) || ''; } function routeToUrl(baseUrl, route) { return new URL(route, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`).toString(); } function px(value) { return Number.parseFloat(String(value || '').replace('px', '')) || 0; } function parseRgb(value) { const match = String(value || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (!match) return null; return match.slice(1, 4).map((item) => parseInt(item, 10) / 255); } function luminance(rgb) { if (!rgb) return 0; const values = rgb.map((channel) => ( channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4 )); return 0.2126 * values[0] + 0.7152 * values[1] + 0.0722 * values[2]; } function contrastRatio(fg, bg) { const a = luminance(parseRgb(fg)); const b = luminance(parseRgb(bg)); const lighter = Math.max(a, b); const darker = Math.min(a, b); return (lighter + 0.05) / (darker + 0.05); } async function collectMetrics(page) { return page.evaluate(({ heroSelectors, titleSelectors, surfaceSelectors }) => { const parseRgbValue = (value) => { const match = String(value || '').match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?/); if (!match) return null; return { rgb: match.slice(1, 4).map((item) => parseInt(item, 10) / 255), alpha: match[4] === undefined ? 1 : Number.parseFloat(match[4]), }; }; const effectiveBackground = (el) => { let current = el; while (current && current.nodeType === Node.ELEMENT_NODE) { const color = getComputedStyle(current).backgroundColor; const parsed = parseRgbValue(color); if (parsed && parsed.alpha > 0.05) return color; current = current.parentElement; } return 'rgb(250, 246, 236)'; }; const luminanceValue = (parsed) => { if (!parsed) return 0; const values = parsed.rgb.map((channel) => ( channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4 )); return 0.2126 * values[0] + 0.7152 * values[1] + 0.0722 * values[2]; }; const contrast = (fg, bg) => { const a = luminanceValue(parseRgbValue(fg)); const b = luminanceValue(parseRgbValue(bg)); const lighter = Math.max(a, b); const darker = Math.min(a, b); return (lighter + 0.05) / (darker + 0.05); }; const width = window.innerWidth; const hero = document.querySelector(heroSelectors); const title = document.querySelector(titleSelectors); const badRadius = []; const badBackground = []; const badChipContrast = []; for (const el of document.querySelectorAll(`.momo-observability-mode :is(${surfaceSelectors})`)) { const style = getComputedStyle(el); const radius = parseFloat(style.borderTopLeftRadius) || 0; if (radius > 8.5) { badRadius.push({ className: String(el.className || '').slice(0, 100), radius, text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80), }); } if ((el.matches(heroSelectors) || el.matches('.obs-panel,.agent-panel,.biz-panel,.runtime-panel,.calls-panel,.gov-panel,.gate-panel,.rag-panel,.qa-panel,.quality-panel,.ppt-panel')) && style.backgroundImage !== 'none') { badBackground.push({ className: String(el.className || '').slice(0, 100), backgroundImage: style.backgroundImage.slice(0, 100), }); } if (badRadius.length >= 8 && badBackground.length >= 8) break; } for (const el of document.querySelectorAll('.momo-observability-mode :is(.badge,.obs-pill,[class$="-pill"],.biz-badge)')) { const style = getComputedStyle(el); const ratio = contrast(style.color, effectiveBackground(el)); if (ratio < 3) { badChipContrast.push({ className: String(el.className || '').slice(0, 100), text: String(el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60), ratio, }); } if (badChipContrast.length >= 8) break; } const titleStyle = title ? getComputedStyle(title) : null; const heroStyle = hero ? getComputedStyle(hero) : null; return { mode: !!document.querySelector('.momo-observability-mode'), finalUrl: location.href, rootOverflow: Math.max(0, document.documentElement.scrollWidth - width), bodyOverflow: Math.max(0, document.body.scrollWidth - width), scrollHeight: document.documentElement.scrollHeight, hero: hero ? { className: String(hero.className || ''), height: Math.round(hero.getBoundingClientRect().height), radius: heroStyle.borderTopLeftRadius, backgroundImage: heroStyle.backgroundImage, } : null, title: title ? { text: String(title.textContent || '').trim().slice(0, 80), fontFamily: titleStyle.fontFamily, fontSize: titleStyle.fontSize, color: titleStyle.color, letterSpacing: titleStyle.letterSpacing, lineHeight: titleStyle.lineHeight, } : null, badRadius, badBackground, badChipContrast, }; }, { heroSelectors: HERO_SELECTORS, titleSelectors: TITLE_SELECTORS, surfaceSelectors: SURFACE_SELECTORS }); } function issuesFor(metrics, viewport) { const issues = []; if (!metrics.mode) issues.push('missing momo-observability-mode'); if (metrics.rootOverflow > 1 || metrics.bodyOverflow > 1) { issues.push(`horizontal overflow root=${metrics.rootOverflow}px body=${metrics.bodyOverflow}px`); } if (!metrics.title) { issues.push('missing title selector'); } else { if (!/Inter|Noto Sans TC/.test(metrics.title.fontFamily)) { issues.push(`title font ${metrics.title.fontFamily}`); } if (px(metrics.title.fontSize) > viewport.maxTitleSize) { issues.push(`title too large ${metrics.title.fontSize}`); } if (metrics.title.letterSpacing !== 'normal' && px(metrics.title.letterSpacing) < -0.1) { issues.push(`title negative letter spacing ${metrics.title.letterSpacing}`); } if (metrics.title.color !== 'rgb(42, 37, 32)') { issues.push(`title color ${metrics.title.color}`); } } if (!metrics.hero) { issues.push('missing hero selector'); } else { if (px(metrics.hero.radius) > 8.5) issues.push(`hero radius ${metrics.hero.radius}`); if (metrics.hero.backgroundImage !== 'none') issues.push('hero background image is not none'); if (metrics.hero.height > viewport.maxHeroHeight) { issues.push(`hero too tall ${metrics.hero.height}px > ${viewport.maxHeroHeight}px`); } } if (metrics.badRadius.length) issues.push(`surface radius offenders ${metrics.badRadius.length}`); if (metrics.badBackground.length) issues.push(`surface background-image offenders ${metrics.badBackground.length}`); if (metrics.badChipContrast.length) issues.push(`chip contrast offenders ${metrics.badChipContrast.length}`); return issues; } async function main() { const options = parseArgs(process.argv.slice(2)); const { chromium } = requirePlaywright(); const launchOptions = { headless: true }; const chromePath = findChromeExecutable(); if (chromePath) launchOptions.executablePath = chromePath; const browser = await chromium.launch(launchOptions); const results = []; try { for (const route of options.routes) { for (const viewport of VIEWPORTS) { const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height }, deviceScaleFactor: 1, }); const url = routeToUrl(options.baseUrl, route); let status = 0; let error = ''; let metrics = null; try { const response = await page.goto(url, { waitUntil: 'networkidle', timeout: options.timeoutMs }); status = response ? response.status() : 0; metrics = await collectMetrics(page); if (new URL(metrics.finalUrl).pathname === '/login') { error = 'redirected to /login'; } else if (status >= 400) { error = `HTTP ${status}`; } } catch (err) { error = err.message || String(err); } const issues = metrics ? issuesFor(metrics, viewport) : []; if (error) issues.unshift(error); results.push({ route, viewport: viewport.name, status, passed: issues.length === 0, issues, metrics }); await page.close(); } } } finally { await browser.close(); } if (options.json) { console.log(JSON.stringify(results, null, 2)); } else { for (const result of results) { const status = result.passed ? 'PASS' : 'FAIL'; const title = result.metrics?.title ? `${result.metrics.title.fontSize} ${result.metrics.title.color}` : 'n/a'; const hero = result.metrics?.hero ? `${result.metrics.hero.height}px r=${result.metrics.hero.radius}` : 'n/a'; console.log(`${status} ${result.viewport} ${result.route} title=${title} hero=${hero}${result.issues.length ? ` issues=${result.issues.join('; ')}` : ''}`); if (!result.passed && result.metrics) { for (const bucket of ['badRadius', 'badBackground', 'badChipContrast']) { for (const offender of result.metrics[bucket] || []) { console.log(` ${bucket}: ${JSON.stringify(offender)}`); } } } } } if (results.some((result) => !result.passed)) { process.exitCode = 1; } } main().catch((error) => { console.error(error.message || error); process.exit(1); });