9cb1e0d242
The value baseline was satisfied by the string appearing anywhere in the built CSS. Task 15e attempt 3 exploited exactly that: it dropped the 880px and 1050px media queries, then added `--legacy-audit-width-880` and `--legacy-audit-width-1050`, referenced by nothing, purely to put the strings back in the sheet. The audit reported success. Two changes close it: - Custom-property declarations nothing references via `var()` are stripped before the value scan. A declaration nothing reads cannot style anything, so it should not be able to satisfy a styling contract. - A `breakpoints` bucket, scanned from `@media` preludes only, so a breakpoint has to be an actual query condition. Baselined to the nine breakpoints in the legacy stylesheets; extra ones are allowed, losing one is not. Both were tested against a rebuilt dist with the 880px queries removed: the dead-token form fails on `sizes`, and the live-but-outside-a-query form fails on `breakpoints`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
3.7 KiB
JavaScript
84 lines
3.7 KiB
JavaScript
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8');
|
|
const pages = [
|
|
'index.html',
|
|
'full-guide/index.html',
|
|
'summary/index.html',
|
|
'models/index.html',
|
|
'agents/index.html',
|
|
'skills/index.html',
|
|
'rules/index.html',
|
|
'skills-review/index.html',
|
|
'hands-on/starter/index.html',
|
|
'hands-on/rules/index.html',
|
|
];
|
|
const walk = (path) =>
|
|
readdirSync(path).flatMap((name) => {
|
|
const child = join(path, name);
|
|
return statSync(child).isDirectory() ? walk(child) : [child];
|
|
});
|
|
const definitions = (css) => new Set([...css.matchAll(/(--[\w-]+)\s*:/g)].map(([, name]) => name));
|
|
const uses = (css) => new Set([...css.matchAll(/var\((--[\w-]+)/g)].map(([, name]) => name));
|
|
const builtCss = walk('dist/_astro')
|
|
.filter((path) => path.endsWith('.css'))
|
|
.map((path) => readFileSync(path, 'utf8'))
|
|
.join('\n');
|
|
// A declaration nothing references cannot style anything. Dropping the dead ones
|
|
// before the value scan is what stops a lost value from being "restored" with an
|
|
// unreferenced token that exists only to put the string back into the sheet.
|
|
const referenced = uses(builtCss);
|
|
const liveCss = builtCss.replace(/(--[\w-]+)\s*:[^;}]*[;}]/g, (declaration, name) =>
|
|
referenced.has(name) ? declaration : '',
|
|
);
|
|
const colorPattern = /#[0-9a-fA-F]{3,8}\b|(?:rgba?|hsla?)\([^)]*\)/g;
|
|
const sizePattern = /\b\d+(?:\.\d+)?(?:px|rem|em|vw|vh)\b/g;
|
|
// Only the media prelude, so a breakpoint has to be an actual query condition.
|
|
const mediaPreludes = (builtCss.match(/@media[^{]+/g) || []).join('\n');
|
|
const builtValues = {
|
|
colors: new Set(liveCss.match(colorPattern) || []),
|
|
sizes: new Set(liveCss.match(sizePattern) || []),
|
|
breakpoints: new Set(mediaPreludes.match(sizePattern) || []),
|
|
};
|
|
if (!builtCss) throw new Error('Astro build emitted no CSS assets to audit');
|
|
// Captured from origin/main after its Astro build. This catches a value that is
|
|
// silently changed to a nearby token: source token checks cannot see that drift.
|
|
const baselineValues = JSON.parse(read('.agents/snapshots/built-css-values.json'));
|
|
|
|
for (const page of pages) {
|
|
const html = read(page);
|
|
if (!html.includes('name="viewport"')) throw new Error(`${page} lacks a viewport declaration`);
|
|
if (html.match(/<(script|link)[^>]+(src|href)="https?:[^\"]+"/i))
|
|
throw new Error(`${page} has an external runtime dependency`);
|
|
}
|
|
for (const file of walk('dist').filter((path) => path.endsWith('.css'))) {
|
|
const css = readFileSync(file, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
if (/@import|src:\s*url\(['"]?https?:|url\(['"]?https?:/i.test(css))
|
|
throw new Error(`${file} has an external CSS dependency`);
|
|
}
|
|
for (const [kind, values] of Object.entries(baselineValues)) {
|
|
const missing = values.filter((value) => !builtValues[kind].has(value));
|
|
if (missing.length) throw new Error(`built CSS lost ${kind}: ${missing.join(', ')}`);
|
|
}
|
|
// Legacy fixtures still ship directly. Resolve every var() from the stylesheets
|
|
// that page actually links; Astro's tokens cannot mask a broken standalone page.
|
|
for (const page of pages) {
|
|
const html = read(page);
|
|
const linked = [...html.matchAll(/<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"/gi)].map(
|
|
([, href]) => href,
|
|
);
|
|
const styles = linked
|
|
.filter((href) => !/^https?:/i.test(href))
|
|
.map((href) => read(join(dirname(page), href)));
|
|
const defined = new Set(styles.flatMap((css) => [...definitions(css)]));
|
|
const unresolved = new Set(
|
|
styles.flatMap((css) =>
|
|
[...uses(css)].filter((name) => name !== '--score' && !defined.has(name)),
|
|
),
|
|
);
|
|
if (unresolved.size)
|
|
throw new Error(`${page} has unresolved CSS variables: ${[...unresolved].join(', ')}`);
|
|
}
|
|
console.log('responsive UI audit passed');
|