Automated Token Audit Scripts for Design Systems

Part of Token Scaling, Validation & CI Pipelines. This page covers the programmatic quality gates that detect structural anomalies, orphaned references, and naming convention violations before they reach production CSS.

Automated token audit scripts serve as the foundational checkpoint within a scaling design system. By scanning token repositories at the AST level, engineering teams catch dangling aliases, deprecated variables still in use, and cross-platform inconsistencies before they propagate to downstream consumers. These scripts operate independently of framework-specific renderers, ensuring design system integrity is maintained across web, mobile, and native platforms simultaneously.

Token Audit Pipeline Flow A flowchart showing a token audit pass: graph scan of the token tree leads to orphan and dangling-alias detection, which feeds into a structured report output. Graph Scan Parse JSON/YAML Build token DAG Detection Orphaned tokens Dangling aliases Naming violations Circular refs Report SARIF / JSON CI gate decision PASS FAIL Step 1 Step 2 Step 3 Token Audit Pass Graph scan → orphan/dangling detection → structured report
A token audit pass: the token graph is scanned and traversed, orphaned and dangling-alias tokens are detected, and a structured SARIF or JSON report drives the CI gate decision.

Problem Framing

Design systems degrade silently. A --ds-color-feedback-error alias that pointed to a deprecated primitive three sprints ago continues to compile without error until someone screenshots a production screen and notices the wrong red. Audit scripts catch this class of failure at commit time, not in QA.

Three failure modes motivate this investment: (1) orphaned tokens — defined in the source dictionary but never referenced in any stylesheet or component; (2) dangling aliases — tokens whose {reference.path} points to a deleted or renamed target; (3) naming drift — tokens that violate the agreed taxonomy after a rushed redesign sprint. None of these failures surface as build errors without explicit audit tooling.

Three-Tier Architectural Trade-offs

  • Regex vs. AST parsing: Regex offers low overhead for flat token files but fails to resolve nested namespaces and circular aliases. AST-based traversal guarantees structural fidelity at the cost of higher initial parse latency — worth the trade-off for any token tree beyond a few hundred entries.
  • Full scan vs. incremental diff: Full scans guarantee 100% coverage but scale poorly with monorepo token growth. Incremental diffing reduces CI time by roughly 70% but requires robust baseline caching and strict branch protection rules.
  • Framework-coupled vs. framework-agnostic: Decoupling audit logic from React/Vue/Angular build steps validates tokens at the source of truth. Framework-coupled checks catch downstream consumption errors but miss upstream schema violations that occur before any framework processes the tokens.
  • Synchronous blocking vs. async reporting: Schema violations must block PR merges immediately. CSS architecture checks can run in parallel with build jobs and only fail the pipeline if critical custom property mappings break — keeping developer feedback loops tight.
  • SARIF vs. custom JSON output: SARIF integrates natively with GitHub Advanced Security and gives inline PR annotations. Custom JSON is easier to pipe into Slack or Datadog but requires bespoke tooling to surface violations inline in the review UI.

Build Pipeline Workflow Steps

  1. Parse source dictionaries. Use json5 or standard JSON.parse to ingest raw token exports from Style Dictionary, Tokens Studio, or a bespoke YAML pipeline.
  2. Construct a hierarchical AST. Map parent-child relationships and preserve metadata — $type, $description, $deprecated — as node attributes.
  3. Resolve alias references. Implement a directed acyclic graph (DAG) resolver to flatten {alias.path} chains. Detect cycles by enforcing a recursion depth limit before throwing.
  4. Build a usage manifest. Scan the consuming codebase — TypeScript components, CSS files, native templates — for every token reference. This manifest is the ground truth for orphan detection, an area explored in depth in detecting orphaned and unused tokens in CI.
  5. Run structural validators. Compare the resolved AST against the JSON schema contract, enforce naming conventions (kebab-case for CSS, camelCase for JS exports), and flag any $deprecated tokens still referenced.
  6. Generate a machine-readable report. Output SARIF for GitHub, JSON for downstream pipeline consumption, or both. Include token path, violation type, severity, and a suggested fix.
  7. Gate the pipeline. Fail the CI job on any error-severity finding. Emit warning-severity findings as annotations without blocking merge when the team is in a grace period.
  8. Cache the audit baseline. Persist the clean AST snapshot to the CI cache so the next incremental run only evaluates modified token branches.
// Core AST Traversal & Alias Resolution (Node.js)
const resolveAliases = (tokens, root = tokens, depth = 0) => {
  if (depth > 10) throw new Error('Circular alias reference detected');

  return Object.entries(tokens).reduce((acc, [key, value]) => {
    if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
      if ('value' in value && typeof value.value === 'string' && value.value.startsWith('{')) {
        // Resolve alias: "{color.primitive.blue.500}" -> look up in root
        const refPath = value.value.replace(/[{}]/g, '').split('.');
        const resolved = refPath.reduce((obj, k) => obj?.[k], root);
        acc[key] = resolved !== undefined ? resolved : value;
      } else {
        acc[key] = resolveAliases(value, root, depth + 1);
      }
    } else {
      acc[key] = value;
    }
    return acc;
  }, {});
};

The depth > 10 guard converts what would be an infinite stack overflow into a named, actionable CI failure. Why this works: JavaScript’s call stack is finite and its error message for stack overflows is opaque; throwing early with context gives engineers a navigable diagnostic.

Validation & Quality Gates

Audit outputs feed directly into Stylelint Plugin Configuration, enabling automated blocking of invalid CSS custom properties at the stylesheet level. The pipeline also integrates with JSON Schema Validation for Tokens to enforce structural contracts before any compilation step runs.

Multi-Stage CI Configuration (GitHub Actions):

name: Token Audit Pipeline
on:
  pull_request:
    paths: ['tokens/**', 'config/schema.json']
  push:
    branches: [main]

jobs:
  audit-tokens:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22', cache: 'npm' }
      - run: npm ci
      - name: Restore audit baseline cache
        uses: actions/cache@v4
        with:
          path: .token-audit-cache
          key: token-audit-${{ hashFiles('tokens/**') }}
      - name: Run Static Schema Validation
        run: npm run audit:schema -- --format sarif --output audit-report.sarif
      - name: Upload SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: 'audit-report.sarif' }
      - name: Incremental CSS Architecture Verification
        run: npm run audit:css -- --diff-base origin/main
      - name: Save audit baseline cache
        uses: actions/cache@v4
        with:
          path: .token-audit-cache
          key: token-audit-${{ hashFiles('tokens/**') }}

Tool reference:

Tool Purpose Integration Point
Style Dictionary Compiles token JSON to platform outputs; exposes hooks for custom validators Pre-compilation transform step
ajv Fast JSON Schema validator; runs against the token dictionary before compilation audit:schema npm script
stylelint + stylelint-design-system-utils Lint CSS files for invalid custom property references Post-compilation lint step
SARIF output adapter Converts audit findings to GitHub-native security annotations upload-sarif Actions step
token-transformer Resolves Tokens Studio aliases to flat W3C DTCG format before audit Ingestion pre-processor

Cross-Cluster Dependency Mapping

Token audit scripts sit at the center of the validation and CI dependency graph. They consume upstream design exports and gate every downstream workflow.

Parent Pillar Sibling Area Integration Point Validation Strategy
Token Scaling & CI Design-to-Code Sync Workflows Audit runs after Figma export, before commit Schema check on raw export payload
Token Scaling & CI JSON Schema Validation for Tokens Schema contract is the audit’s primary ruleset ajv strict mode with additionalProperties: false
Token Scaling & CI Stylelint Plugin Configuration Audit report drives which custom properties Stylelint allows Allowlist regenerated from clean audit artifact
Token Scaling & CI Versioning & Semantic Release for Tokens Clean audit is a prerequisite for a semantic release Audit exit code gates the release:tokens script
Token Scaling & CI Token Compiler Comparison Compiler choice affects AST shape the audit must understand Audit schema adapters per compiler output format
/* tokens/semantic/color.css */
/* @depends: tokens/primitive/color.json — audit will fail if primitives change shape */
:root {
  --ds-color-action-primary: var(--ds-primitive-blue-600);
  --ds-color-feedback-error: var(--ds-primitive-red-500);
}

Production Code Reference

The framework-agnostic gating script runs after every merge to main, sequencing schema validation before CSS verification so a broken schema never reaches the more expensive CSS analysis step.

#!/bin/bash
# scripts/post-merge-audit.sh
set -e

echo "Running post-merge token audit..."

# 1. Validate schema — fast, blocks immediately on structural errors
npm run audit:schema

# 2. Check CSS mapping accuracy — more expensive, runs only if schema is clean
npm run audit:css

echo "Audit passed. Triggering semantic release..."
npm run release:tokens

The set -e flag ensures any non-zero exit code from either audit step aborts the script before the release runs. Why this works: without set -e, a failed audit:schema silently exits and the release:tokens command still executes, publishing broken tokens.

Orphan detection snippet — cross-references the resolved AST against the usage manifest:

// scripts/audit-orphans.js
import { readTokenAST } from './lib/ast.js';
import { buildUsageManifest } from './lib/manifest.js';

const tokens = await readTokenAST('./tokens');
const used = await buildUsageManifest(['./src/**/*.{ts,tsx,css}']);

const orphans = Object.keys(tokens).filter(key => !used.has(key));

if (orphans.length > 0) {
  console.error('Orphaned tokens detected:');
  orphans.forEach(t => console.error(`  - ${t}`));
  process.exit(1);
}

console.log(`Audit passed. ${Object.keys(tokens).length} tokens, 0 orphans.`);

Diagnostic Matrix

Diagnostic Step Execution Detail
Confirm AST parses cleanly Run npm run audit:schema -- --dry-run and inspect the JSON output for null nodes — these indicate malformed alias paths
Verify alias resolver depth limit Introduce a deliberate circular alias in a test fixture; confirm the audit throws Circular alias reference detected at depth 10
Check usage manifest coverage Print manifest.size and compare to the known token count; a manifest far smaller than the token count indicates glob patterns are not matching component files
Confirm SARIF uploads to GitHub Inspect the repository Security tab after a failing PR; missing annotations mean the upload-sarif step ran before the audit generated the file
Validate incremental diff accuracy Delete a token that is known to be used, then run audit:css -- --diff-base HEAD~1; confirm it surfaces the deletion as an error, not a warning

Root causes and resolutions:

Symptom Root Cause Resolution
Orphan detection reports zero orphans on a clearly stale token file Usage manifest glob does not cover all consumer file extensions Add *.vue, *.svelte, or native template extensions to the manifest scanner config
Circular alias error on a token that has no self-reference Two tokens reference each other indirectly through three or more hops Flatten the alias chain in the source dictionary; use a primitive directly
SARIF file not appearing in GitHub Security tab upload-sarif step runs before audit:schema generates the file Add if: always() to the upload step and confirm the file path matches the --output flag
Incremental diff misses a renamed token as a violation Baseline cache is stale from before the rename Delete .token-audit-cache and run a full scan to rebuild the baseline
Naming convention check passes for a token that uses camelCase Regex pattern only checks top-level keys, not nested namespace segments Update the convention validator to recurse into nested objects and validate every path segment

FAQ

Q: Should the audit script run before or after Style Dictionary compilation?

Run the structural schema validation before compilation and the CSS mapping verification after. Schema checks are fast and catch the errors most likely to break compilation — running them first prevents wasting minutes on a compilation step that was doomed from the start. The CSS verification step requires compiled output, so it necessarily runs second.

Q: How do you handle tokens that are intentionally defined but not yet used?

Add a $status: reserved metadata field to the W3C DTCG token definition and configure the orphan detector to skip tokens carrying that flag. This gives teams a way to pre-declare tokens in the dictionary before the consuming components are built, without triggering false-positive audit failures.

Q: What is the right threshold for blocking a PR versus warning?

Block on any error-severity finding: dangling aliases, schema violations, and naming convention failures that would produce invalid CSS. Warn on warning-severity findings: orphaned tokens (they may be used in a platform the manifest scanner does not cover), deprecated tokens still in use (they need a migration window), and tokens with missing $description fields. Revisit warning thresholds quarterly as the system matures.