Files
ai-for-dummies/scripts/audit-ui.mjs
T
Marcos Paulo d88d8b89eb
verify-and-publish / gate (push) Successful in 7m30s
verify-and-publish / publish (push) Has been skipped
refactor: cut over to the Astro build
Merges refactor/task-20-cutover. Task 20 steps 1, 2, and 5; publishing is
not included.

The hand-written site is gone: 32 files deleted, including app.js,
responsive.css, and all ten route index.html files. Twelve more could not
be deleted -- the Astro pages import them and the build fails without
them -- so they moved to legacy/ verbatim, outside the reach of
check-tokens.mjs, which sweeps src/ and would demand a token migration
these files have not had.

Before anything was deleted, rendered-text-diff swept all ten routes plus
both Portuguese pages at full parity, 0 missing and 0 extra. That
comparison stops being possible once the legacy files are gone, which is
why it ran first. computed-style-diff on /full-guide/ is unchanged at 32.

verify.mjs no longer reads app.js and holds at 84 assertions.
audit-ui.mjs reads dist/. Docs across README, AGENTS.md, GATES.md, the
architecture context, and the operations guide now describe the built
site rather than the hand-written one.

origin/pages is unchanged at 37a1e480c6.
The publish job is still gated to manual dispatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 08:31:33 +00:00

91 lines
4.2 KiB
JavaScript

import { readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
const base = '/ai-for-dummies/';
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8');
// The legacy hand-written pages are gone; the built output is the site now.
const pages = [
'dist/index.html',
'dist/full-guide/index.html',
'dist/summary/index.html',
'dist/models/index.html',
'dist/agents/index.html',
'dist/skills/index.html',
'dist/rules/index.html',
'dist/skills-review/index.html',
'dist/hands-on/starter/index.html',
'dist/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(', ')}`);
}
// The hands-on labs still ship their own stylesheet rather than going through
// Astro. Resolve every var() from the stylesheets each page actually links, so
// Astro's tokens cannot mask a broken standalone page.
// Astro emits base-absolute hrefs (`/ai-for-dummies/_astro/x.css`); those are
// rooted at `dist/`, not at the page's own directory.
const resolveHref = (page, href) =>
href.startsWith(base) ? join('dist', href.slice(base.length)) : join(dirname(page), href);
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(resolveHref(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');