diff --git a/theme/package.json b/theme/package.json index ce63df3..741e8a2 100644 --- a/theme/package.json +++ b/theme/package.json @@ -13,6 +13,8 @@ "send": "mkdir -p dist/fonts && cp -r fonts/geist fonts/space-grotesk dist/fonts/", "sheet": "npm run build && echo 'open theme/src/sheet.html in browser ( Consumes theme/dist/rams.css )'", "basemap": "node basemap/build.mjs", - "basemap:preview": "python3 -m http.server 8123" + "basemap:preview": "python3 -m http.server 8123", + "audit": "node ./scripts/audit.mjs ./dist/rams.css", + "test": "node ./scripts/audit.test.mjs" } } diff --git a/theme/scripts/audit.mjs b/theme/scripts/audit.mjs new file mode 100644 index 0000000..bbfe066 --- /dev/null +++ b/theme/scripts/audit.mjs @@ -0,0 +1,566 @@ +#!/usr/bin/env node +/* + * audit.mjs — automated off-token audit (D-13) + * ------------------------------------------------------------------ + * "Zero off-token values" is evidence, not an eyeball claim: this + * script scans RENDERED CSS — every declaration the implementation + * actually wears — and flags any color / typography / radius / + * shadow / blur value that does not derive from + * theme/src/tokens.css (the single source of truth, D-07). + * + * Usage: + * node scripts/audit.mjs [--tokens ] [--pages ] [css...] + * --tokens token sheet to build the allowlist from + * (default: ./src/tokens.css) + * --pages implementation pages that define the used-class + * harvest (default: the IMPLEMENTATION_PAGES registry + * below — later UI phases APPEND their pages there) + * css... CSS inputs to audit (default: ./dist/rams.css) + * + * Exit codes (no silent passes — T-1-07): + * 0 clean — every checked declaration derives from tokens + * 1 violations — each printed as file:line: declaration + * 2 drift/error — token sheet degraded (see message) + * + * ——— Scope adjudications (documented, deliberate) ——— + * + * 1. RENDERED, not compiled-vocabulary (D-06/D-13): dist/rams.css + * compiles the mockup screens' utility vocabulary 1:1 (wave-1 + * @source decision) — including utilities the mockups wear but + * the Rams design doc REJECTS (shadow-*, blur, backdrop-blur, + * arbitrary bg-[#…] values). Mockup markup is layout reference + * only; the implementation never wears those classes. So within + * @layer utilities only rules whose selectors match a class + * actually used by an implementation page are audited. If a + * later phase lets shadow/backdrop-blur/bg-black into its + * markup, those classes enter the harvest and get flagged. + * + * 2. Framework layers are skipped: @layer properties / theme / + * base are Tailwind's runtime plumbing (—tw-* registrations, + * preflight normalization) plus the token emission itself (the + * allowlist). @layer components, unlayered CSS, and every other + * layer are implementation-owned and fully audited. --tw-* + * custom properties are plumbing in any layer and are skipped. + * + * 3. Token-hue + alpha is sanctioned: 8-digit hex whose first six + * digits are a token value (e.g. matte-black at reduced opacity) + * derives from the package; Tailwind's own black (#000000xx) is + * NOT a token and stays flagged. + * + * 4. rounded-full is a circle primitive, not a roundedness degree: + * the design doc's Shapes rule enumerates rect families (cards, + * buttons, banners, badges, containers) — all of which this audit + * enforces at absolute zero. The mockups' circular signets + * (position puck, speed medallion, status dots) are drawn + * circles: spec geometry, like the 18x18 checkbox square. Only + * the full-circle primitive (calc(infinity*1px) and its + * minified >=1e30px form) is sanctioned; every other non-zero + * radius — including 50% — is flagged. + * + * 5. rgb()/rgba()/hsl()/hsl() literals are checked too (hex is not + * the only escape hatch for off-token colors). var() references + * are always token paths and pass by construction. + * + * Reuse (D-14 lineage): later UI phases extend IMPLEMENTATION_PAGES + * with their pages and keep running `npm run audit` in CI/local + * gates — the DSGN-01 evidence command for every phase. + */ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SCRIPTS = dirname(fileURLToPath(import.meta.url)); +const THEME_ROOT = resolve(SCRIPTS, '..'); +const REPO_ROOT = resolve(THEME_ROOT, '..'); +const DEFAULT_TOKENS = join(THEME_ROOT, 'src', 'tokens.css'); +const DEFAULT_INPUT = join(THEME_ROOT, 'dist', 'rams.css'); + +/* Drift guard floor (T-1-07): the Rams token scale has 55 colors; + * a parse far below that means tokens.css degraded silently. */ +const COLOR_FLOOR = 40; + +/* Framework-owned cascade layers (see adjudication 2). */ +const FRAMEWORK_LAYERS = new Set(['properties', 'theme', 'base']); +const UTILITY_LAYER = 'utilities'; + +/* At-rules that wrap nested blocks rather than declare anything. */ +const WRAPPER_AT = new Set([ + 'layer', 'media', 'supports', 'container', 'scope', + 'keyframes', 'starting-style', +]); + +/* + * Implementation pages (the rendered surface — adjudication 1). + * EVERY page that renders Rams components belongs here; later UI + * phases append theirs. Inline