#!/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