#!/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 / spacing / 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()/hsla() literals are checked too (hex is not * the only escape hatch for off-token colors) — as are named color * keywords (on color-bearing properties; currentcolor/inherit/ * transparent stay sanctioned) and modern color functions — * oklch()/oklab()/lab()/lch()/hwb()/color()/color-mix(). var() * references are always token paths and pass by construction. * * 6. Spacing numerics (WR-04) are audited on the spacing-scale * properties — the padding/margin/gap families, logical variants * included: literals must equal a --spacing-* token value, zero, * or auto. Dimensional geometry — width/height, positioning * offsets, border widths, the 18x18 checkbox square — is spec * geometry per the components.css header, not spacing-scale * consumption. * * 7. Typography numerics (WR-04) — font-size/font-weight/line-height/ * letter-spacing — are audited against the --type-* scale; the * normal/bold keywords ≡ initial/≡ the 700 scale weight and stay * sanctioned. @font-face blocks describe the self-hosted font * binaries (D-16): their font-weight values are asset metadata, * not rendered typography, and are exempt from the numeric scale * (color and font-family checks still apply). * * 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