Files
HMDesign/scripts/guard-style-evidence.mjs

47 lines
4.8 KiB
JavaScript

import { verdict } from './guard-style-analysis.mjs';
// Exact source sites, not a single browser PASS for all CSS / dynamic branches.
export function styleVerification(guard, runtime, cssEvidence, linked) {
const widths = [390, 768, 1280, 1440];
const dynamic = (guard.tokenEvidence ?? []).flatMap(e => (e.dynamic ?? []).map(d => ({ ...d, definitions: e.definitions ?? [], incomplete: e.incomplete })));
const sites = [...(guard.styleEvidence?.checks ?? []).filter(c => c.designIntentStatus !== 'PASS'), ...dynamic];
const checks = [];
const supplied = runtime.cssEvidence?.checks;
for (const site of sites) {
const measurements = [];
for (const raw of Array.isArray(supplied) ? supplied.filter(c => c?.siteId === site.id) : []) {
const named = cssEvidence.checks.find(c => c.name === raw.name && c.width === raw.width && c.selector === raw.selector && c.property === raw.property && c.actual === raw.actual && c.expected === raw.expected);
const browser = raw.browser;
const meta = named && linked && raw.file === site.file && raw.ruleId === site.ruleId &&
typeof raw.state === 'string' && raw.state.trim() && typeof raw.component === 'string' && raw.component.trim() &&
browser && typeof browser.name === 'string' && browser.name.trim() && typeof browser.version === 'string' && browser.version.trim() &&
Number.isFinite(raw.height) && raw.height > 0 &&
(!site.selector || raw.selector === site.selector || raw.sourceSelector === site.selector && raw.matchesSourceSelector === true) &&
(!site.component || raw.component.toLowerCase().replaceAll('-', '') === site.component.toLowerCase().replaceAll('-', ''));
if (!meta) { measurements.push({ ...raw, status: 'NOT VERIFIED', reason: 'Missing / mismatched site, source, rule, DOM, state, viewport or browser metadata' }); continue; }
if (site.ruleId === 'TOKEN-002') {
const expression = raw.actualExpression;
const names = typeof expression === 'string' ? [...expression.matchAll(/(?:var\(\s*|\()(--hm-[\w-]+)\s*(?=[,)])/g)].map(m => m[1]) : [];
const validTokens = names.length > 0 && Array.isArray(raw.tokens) && names.every(n => raw.tokens.includes(n));
const missing = names.filter(n => !site.definitions.includes(n));
const malformed = typeof expression === 'string' && /var\(\s*--hm-[\w-]+\s+[^\s,)]/.test(expression);
const tokenStatus = malformed ? 'FAIL' : !validTokens || site.incomplete ? 'NOT VERIFIED' : missing.length ? 'FAIL' : 'PASS';
measurements.push({ ...raw, status: verdict([named.status, tokenStatus]), reason: missing.length ? `Observed undefined Token: ${missing.join(', ')}` : !validTokens ? 'Actual dynamic expression / token references were not supplied' : 'Observed declarations and computed value checked; unobserved branches remain unverified' });
} else {
const sameProperty = raw.property.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`) === site.property;
measurements.push({ ...raw, status: sameProperty ? named.status : 'NOT VERIFIED', reason: sameProperty ? 'Measured actual / expected equality at named DOM state' : 'Measurement property does not match the declaration' });
}
}
const covered = widths.every(w => measurements.some(m => m.width === w && m.status === 'PASS'));
const runtimeStatus = measurements.some(m => m.status === 'FAIL') ? 'FAIL' : covered && measurements.every(m => m.status === 'PASS') ? 'PASS' : 'NOT VERIFIED';
const staticStatus = site.staticStatus ?? site.status;
const designIntentStatus = site.designIntentStatus ?? 'PASS';
checks.push({ id: site.id, ruleId: site.ruleId, file: site.file, line: site.line, component: site.component ?? null, selector: site.selector ?? null, property: site.property ?? null, tokens: site.tokens ?? [], staticStatus, runtimeStatus, designIntentStatus, finalStatus: verdict([staticStatus, runtimeStatus, designIntentStatus]), measurements, reason: site.reason ?? 'Dynamic source cannot prove all branches; browser observations are separate evidence' });
}
const failures = (guard.issues ?? []).filter(i => !i.code.startsWith('RENDER-') && !['REUSE-003', 'TOKEN-002'].includes(i.code)).map(i => i.severity === 'FAIL' ? 'FAIL' : 'NOT VERIFIED');
const staticStatus = verdict([...failures, ...checks.map(c => c.staticStatus)]);
const runtimeStatus = sites.length ? verdict(checks.map(c => c.runtimeStatus)) : 'NOT VERIFIED';
const designIntentStatus = verdict([...checks.map(c => c.designIntentStatus), ...(guard.issues ?? []).filter(i => i.code === 'REUSE-003').map(() => 'NOT VERIFIED')]);
return { staticStatus, runtimeStatus, designIntentStatus, finalStatus: verdict([staticStatus, runtimeStatus, designIntentStatus]), checks, coverage: 'Named DOM / properties / four widths / supplied states only; unobserved themes, states and external inputs remain NOT VERIFIED' };
}