# How DESIGN.md Token Reference Resolution Works Internally

> Understand DESIGN.md token reference resolution's three-phase pipeline: symbol tables, recursive reference resolution with cycle detection, and application to component properties with error reporting.

- Repository: [Google Labs Code/design.md](https://github.com/google-labs-code/design.md)
- Tags: internals
- Published: 2026-06-28

---

**DESIGN.md token reference resolution operates through a three-phase pipeline where the `ModelHandler` class first builds a symbol table of primitive values, then recursively resolves chained references like `{colors.primary}` while detecting cycles, and finally applies these tokens to component properties with full error reporting.**

The google-labs-code/design.md repository implements a sophisticated token resolution system that transforms curly-brace references in DESIGN.md files into concrete design values. This article examines how the **DESIGN.md token reference resolution** mechanism processes `{path.to.token}` syntax through the CLI linter's model handler, from initial parsing to final component property assignment.

## The Three-Phase Resolution Pipeline

The resolution logic lives in the `ModelHandler` class within [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts). This handler orchestrates the transformation of raw YAML into a fully resolved design system through distinct phases.

### Phase 1: Primitive Token Parsing and Symbol Table Construction

The process begins by loading **spec limits** from [`spec-config.yaml`](https://github.com/google-labs-code/design.md/blob/main/spec-config.yaml) via `getSpecConfig()` in [`packages/cli/src/linter/spec-config.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/spec-config.ts) (lines 24‑27). The handler then iterates over top-level token groups—`colors`, `typography`, `rounded`, and `spacing`—to construct an initial **symbol table**.

For each entry in the parsed YAML (lines 54‑124 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)):

- **Primitive values** (hex colors, dimensions) are parsed using type-specific functions like `parseColor` or `parseDimension` and inserted as resolved objects.
- **Reference strings** (e.g., `"{colors.primary}"`) are inserted as-is for later resolution.

This creates a hybrid symbol table where some keys point to concrete `ResolvedColor` or `ResolvedDimension` objects, while others point to raw reference strings awaiting the second phase.

### Phase 2: Chained Reference Resolution with Cycle Detection

Once the initial table is built, the handler invokes `resolveReference(symbolTable, path, visited, depth)` for every entry that matches the reference pattern (lines 73‑96 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)). This recursive function implements the core **DESIGN.md token reference resolution** algorithm:

1. **Depth checking**: Immediately returns `null` if `depth` exceeds `MAX_REFERENCE_DEPTH` (default 10).
2. **Cycle detection**: Maintains a `visited` Set to track paths already traversed; detects circular references like `a → b → c → a`.
3. **Path lookup**: Retrieves the current value from `symbolTable` using the dot-separated path.
4. **Recursive resolution**: If the looked-up value is another reference string, the function recurses with the inner path.
5. **Object return**: Returns the final resolved primitive object or `null` on failure.

When resolution succeeds, the symbol table entry is replaced with the resolved object (lines 128‑176 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)). Failures—whether from missing tokens, cycles, or depth violations—are recorded in the `findings` array for error reporting.

### Phase 3: Component Property Resolution

The final phase processes **component definitions** (lines 78‑112 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)). For each property in a component:

- Numeric and boolean values pass through unchanged.
- Values matching the reference pattern trigger another call to `resolveReference` against the fully populated symbol table.
- Successful lookups replace the raw reference with the resolved token object.
- Unresolved references are collected in `unresolvedRefs` for reporting.

This ensures that component properties like `backgroundColor` or `padding` contain fully resolved objects (e.g., `ResolvedColor`) rather than reference strings, making them ready for downstream generators like Tailwind configs.

## Configuration Limits and Safety Guards

The resolution engine enforces strict boundaries to prevent runaway recursion and excessive nesting, defined in [`spec-config.yaml`](https://github.com/google-labs-code/design.md/blob/main/spec-config.yaml) and exposed through [`packages/cli/src/linter/spec-config.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/spec-config.ts):

- **`MAX_REFERENCE_DEPTH`** (default 10): Limits the number of hops in a reference chain. Prevents infinite recursion when tokens reference each other in long chains.
- **`MAX_TOKEN_NESTING_DEPTH`** (default 20): Restricts how deep objects can nest in the YAML tree, enforced by `forEachLeaf` to maintain sane token structures.

These constants are loaded at initialization and checked during the resolution process to ensure the linter terminates predictably even with malformed input.

## Practical Implementation Examples

### Resolving a Simple Color Reference

Consider a DESIGN.md file with a reference:

```yaml
colors:
  primary: "#1A1C1E"
  accent: "{colors.primary}"

```

The `ModelHandler` executes:

```typescript
// Phase 1: Build symbol table
symbolTable.set('colors.primary', resolvedPrimary);      // ResolvedColor
symbolTable.set('colors.accent', '{colors.primary}');   // Raw string

// Phase 2: Resolve reference
const resolved = resolveReference(
  symbolTable, 
  'colors.primary', 
  new Set(), 
  0
);
// Returns ResolvedColor for "#1A1C1E"
symbolTable.set('colors.accent', resolved);

```

The `accent` token now points to the same `ResolvedColor` instance as `primary`.

### Detecting Circular Reference Chains

The resolver detects cycles through the `visited` Set:

```yaml
colors:
  a: "{colors.b}"
  b: "{colors.c}"
  c: "{colors.a}"  # Creates cycle

```

Execution trace:

```

resolveReference('colors.a', visited=[], depth=0)
 → sees '{colors.b}' → recurse('colors.b', visited=['a'], depth=1)
    → sees '{colors.c}' → recurse('colors.c', visited=['a','b'], depth=2)
       → sees '{colors.a}' → 'a' in visited → return null

```

The model generates a finding:

```typescript
{
  severity: 'error',
  path: 'colors.a',
  message: 'Reference could not be resolved (missing or circular).'
}

```

### Processing Component Properties

Component definitions resolve against the complete symbol table:

```yaml
components:
  card:
    backgroundColor: "{colors.primary}"
    padding: "{spacing.md}"

```

During component handling (lines 78‑112 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)):

```typescript
if (isTokenReference(rawValue)) {
  const refPath = rawValue.slice(1, -1); // "colors.primary"
  const resolved = resolveReference(symbolTable, refPath, new Set(), 0);
  
  if (resolved !== null) {
    properties.set(propName, resolved);
  } else {
    unresolvedRefs.push(rawValue);
  }
}

```

The resulting `ComponentDef` contains resolved objects ready for code generation.

### Programmatic CLI Usage

Invoke the resolver directly in TypeScript:

```typescript
import { ModelHandler } from './packages/cli/src/linter/model/handler.js';
import { parseDesignSystem } from './packages/cli/src/linter/parser/spec.js';

const rawDesign = await readFile('DESIGN.md', 'utf8');
const parsed = parseDesignSystem(rawDesign);
const result = new ModelHandler().execute(parsed);

if (result.findings.length) {
  console.error('Validation errors:', result.findings);
}
console.log('Resolved tokens:', [...result.designSystem.colors.entries()]);

```

This demonstrates the complete pipeline: parsing YAML → building the model → resolving references → emitting findings.

## Summary

- **DESIGN.md token reference resolution** uses a three-phase pipeline in the `ModelHandler` class to transform `{path.to.token}` syntax into concrete values.
- The system builds a **symbol table** in [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts) (lines 54‑124), then recursively resolves chained references via `resolveReference()` (lines 73‑96) with cycle detection and depth limits.
- **Safety guards** (`MAX_REFERENCE_DEPTH`, `MAX_TOKEN_NESTING_DEPTH`) prevent infinite recursion and excessive nesting, configured in [`spec-config.yaml`](https://github.com/google-labs-code/design.md/blob/main/spec-config.yaml) and loaded via [`spec-config.ts`](https://github.com/google-labs-code/design.md/blob/main/spec-config.ts).
- **Component properties** resolve against the fully populated symbol table, with failures tracked in `unresolvedRefs` for reporting.
- All errors—including invalid formats, missing references, and circular dependencies—are accumulated in the `findings` array and returned with the model result.

## Frequently Asked Questions

### How does the resolver prevent infinite loops with circular token references?

The `resolveReference` function in [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts) tracks visited paths using a `Set` parameter. Before recursing into a new reference, it checks if the path already exists in the set. If detected, the function returns `null` immediately and the model records a circular reference error in the findings array.

### What is the maximum depth for token reference chains?

The default **maximum reference depth** is 10 hops, defined by the `MAX_REFERENCE_DEPTH` constant in [`packages/cli/src/linter/spec-config.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/spec-config.ts). This limit prevents runaway recursion when tokens reference each other in long chains. You can modify this value in [`spec-config.yaml`](https://github.com/google-labs-code/design.md/blob/main/spec-config.yaml) if your design system requires deeper nesting.

### Can component properties reference tokens that themselves reference other tokens?

Yes. The resolution pipeline handles **chained references** transparently. When processing component properties in phase 3, the resolver calls the same `resolveReference` function used for top-level tokens. This function recursively follows reference chains (e.g., `{colors.accent}` → `{colors.primary}` → `#1A1C1E`) until reaching a primitive value or hitting a cycle/depth limit.

### Where are the resolved token values stored after processing?

Resolved values replace the original entries in the **symbol table** maintained by the `ModelHandler`. Specifically, lines 128‑176 in [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts) update the symbol table and dedicated maps (`colors`, `rounded`, `spacing`) with the resolved objects (`ResolvedColor`, `ResolvedDimension`, etc.), ensuring downstream generators receive concrete values rather than reference strings.