How to Debug Why a Token Reference Isn't Resolving in the Design MD CLI

To debug an unresolved token reference in the Design MD CLI, verify the syntax with isTokenReference, inspect the symbolTable for the target key, and trace the resolveReference function in packages/cli/src/linter/model/handler.ts to detect missing keys, circular references, or depth limit violations.

The Design MD CLI from the google-labs-code/design.md repository processes design-token references like {colors.brand} through a multi-phase resolution pipeline. When a token reference isn't resolving, the issue typically stems from syntax errors, missing symbol table entries, or circular dependencies in the token chain. Understanding how the CLI resolves these references allows you to pinpoint exactly where the resolution breaks down.

Understanding the Three-Phase Resolution Pipeline

The CLI resolves design-token references in three distinct phases:

  1. Primitive token parsing – Raw values are stored in a symbolTable alongside any already-resolved values.
  2. Chained reference resolution – The CLI walks the symbol table, replacing token-reference strings with concrete values using the resolveReference function (lines 73-98 in packages/cli/src/linter/model/handler.ts).
  3. Component construction – Component properties are populated with resolved values, and any reference that could not be resolved is recorded in the unresolvedRefs array (lines 84-101).

Common Root Causes of Unresolved Token References

When resolveReference returns null, it typically indicates one of these five issues:

Invalid token syntax – If isTokenReference returns false, the string lacks proper curly braces {} and is stored unchanged in the symbol table, bypassing resolution entirely.

Non-existent target entry – When symbolTable.get(path) returns null inside resolveReference, the reference path does not exist in the symbol table, causing the token to be added to unresolvedRefs.

Circular reference – The cycle detection mechanism (visited.has(path)) identifies when a token references itself through a chain, returning null and leaving the original reference unresolved.

Depth limit exceeded – The MAX_REFERENCE_DEPTH guard (defined in packages/cli/src/linter/spec-config.ts at line 31) aborts resolution when chained references exceed the default limit of 20 levels.

Type mismatch – After resolution, the caller verifies resolved.type (e.g., lines 133-137 for color validation). If the resolved token type does not match the expected property type, the value is ignored and the reference remains unresolved.

Step-by-Step Debugging Guide

Follow these steps to isolate why a specific token reference is failing:

  1. Confirm the token syntax

Verify that your token uses proper curly-brace notation:

import { isTokenReference } from './packages/cli/src/linter/model/spec.js';

const token = '{colors.brand}';
console.log(isTokenReference(token)); // Must return true

If isTokenReference returns false, the CLI will never attempt resolution.

  1. Inspect the symbol table after Phase 1

Insert a temporary debug statement after the primitive parsing loops (around line 74 in handler.ts):

console.log('Symbol Table:', symbolTable);

Look for entries like colors.brand → '{colors.base}' (indicating a chained reference) or a concrete value.

  1. Check that the target key exists

Extract the path and verify its presence:

const path = token.slice(1, -1); // "colors.brand"
console.log(symbolTable.has(path)); // Must return true
  1. Run the resolver manually

Test the resolution logic directly:

import { resolveReference } from './packages/cli/src/linter/model/handler.js';

const resolved = resolveReference(symbolTable, 'colors.brand', new Set());
console.log('Resolved:', resolved);

If resolved is null, the key is missing, a cycle was detected, or the depth limit was exceeded.

  1. Detect cycles or deep chains

Monitor the visited Set while stepping through resolveReference. For depth issues, check if your chain exceeds MAX_REFERENCE_DEPTH (20 levels by default). Increase this constant in spec-config.ts only after confirming the design intent.

  1. Verify the final type

Ensure the resolved token type matches the consumer's expectation. If you expect a color but receive a dimension, the handler will skip the value (see lines 133-137 in handler.ts).

  1. Read the unresolvedRefs array

After model construction, inspect the unresolvedRefs array populated at line 98:

console.log('Unresolved:', component.unresolvedRefs);

This array contains all references that survived the resolution pipeline.

  1. Run the unit tests

Execute the test suite in packages/cli/src/linter/model/handler.test.ts (specifically lines 225-282) to verify resolution behavior against known examples:

npm test

Typical Pitfalls to Avoid

Watch for these common mistakes that prevent token resolution:

  • Whitespace or extra characters – The isTokenReference function requires exact {...} syntax without surrounding spaces.
  • Case-sensitivity – Keys are case-sensitive; colors.Brand does not match colors.brand.
  • Missing section prefix – References must include the full path including section prefixes like colors. or spacing..

Quick Sanity Check Script

Run this standalone script inside the repository root to verify token resolution:

import { readFileSync } from 'fs';
import { isTokenReference } from './packages/cli/src/linter/model/spec.js';
import { resolveReference } from './packages/cli/src/linter/model/handler.js';

// Assuming symbolTable is built from your DESIGN.md or token JSON
const token = '{colors.brand}';

if (!isTokenReference(token)) {
  console.error('Token syntax invalid');
} else {
  const path = token.slice(1, -1);
  const resolved = resolveReference(symbolTable, path, new Set());
  console.log('Resolved value:', resolved);
}

Key Source Files for Reference

Understanding these files accelerates debugging:

Summary

  • The Design MD CLI resolves tokens through primitive parsing, chained resolution via resolveReference, and component construction phases.
  • Unresolved references typically result from invalid syntax, missing symbol table entries, circular dependencies, depth limit violations, or type mismatches.
  • Debug by verifying isTokenReference returns true, checking symbolTable.has(path), and tracing resolveReference for null returns.
  • Inspect unresolvedRefs on components and consult handler.test.ts for expected resolution behavior.
  • Reference spec-config.ts to adjust MAX_REFERENCE_DEPTH when dealing with deeply nested token chains.

Frequently Asked Questions

Why does my token reference remain as plain text in the output?

If a token appears as literal text like {colors.brand} instead of resolving to a value, isTokenReference likely returned false due to malformed syntax or extra whitespace. The CLI stores the string unchanged in the symbol table when it fails the curly-brace validation check defined in packages/cli/src/linter/model/spec.ts.

How do I fix a circular reference error?

Circular references occur when token A references token B, which eventually references token A again. The resolveReference function detects this using a visited Set and returns null. Break the cycle by editing your DESIGN.md file to remove the recursive dependency, ensuring tokens form a directed acyclic graph.

Can I increase the maximum depth for chained token references?

Yes. The default limit of 20 chained references is defined as MAX_REFERENCE_DEPTH in packages/cli/src/linter/spec-config.ts at line 31. Increase this constant only after confirming that your design system intentionally requires deeper nesting, as excessive chaining can indicate architectural issues.

Where does the CLI report which references failed to resolve?

The CLI populates the unresolvedRefs array on each component during the construction phase (lines 84-101 in packages/cli/src/linter/model/handler.ts). Inspect this array in your component output or test results to see exactly which token paths could not be resolved.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →