# How Token Reference Resolution Works in DESIGN.md Components

> Learn how DESIGN.md components resolve token references using a three-phase pipeline in ModelHandler. Discover cycle detection and depth limits for concrete design values.

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

---

**Token reference resolution in DESIGN.md uses a three-phase pipeline within the `ModelHandler` class to transform `{path.to.token}` syntax into concrete design values, enforcing cycle detection and configurable depth limits.**

The [`google-labs-code/design.md`](https://github.com/google-labs-code/design.md/blob/main/google-labs-code/design.md) CLI linter resolves token references through a sophisticated symbol table mechanism. This process converts curly-brace syntax like `{colors.primary}` into resolved color objects, dimensions, or typography tokens. Understanding this resolution pipeline is essential for debugging complex design systems and avoiding circular dependency errors.

## The Three-Phase Resolution Pipeline

Token reference resolution operates sequentially across three distinct phases, all orchestrated by the `ModelHandler` class 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).

### Phase 1: Building the Symbol Table

The resolver first constructs a **symbol table** by iterating over top-level token groups including `colors`, `typography`, `rounded`, and `spacing`. During this phase (lines 54–124 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)), the handler distinguishes between primitive values and reference strings.

- **Primitive values** (e.g., `#1A1C1E`, `16px`) are parsed using type-specific functions like `parseColor` or `parseDimension` and stored as **resolved objects**.
- **Reference strings** (e.g., `{colors.primary}`) are inserted into the symbol table as raw strings for later processing.

This initial pass ensures that all potential target tokens exist in the lookup table before resolution begins.

### Phase 2: Resolving Chained References

Once the symbol table is populated, the handler invokes `resolveReference(symbolTable, path, visited, depth)` for every entry still containing a reference string (lines 73–96 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)). This recursive function implements the core resolution logic:

1. **Depth checking**: The function immediately returns `null` if `depth` exceeds `MAX_REFERENCE_DEPTH` (default 10).
2. **Cycle detection**: A `visited` Set tracks all paths encountered during the current recursion chain. If a path appears twice, the function detects a circular reference and aborts.
3. **Path lookup**: The dot-separated path (e.g., `colors.primary`) is queried against the symbol table.
4. **Recursive descent**: If the looked-up value is another reference string, the function recurses with the new path.
5. **Resolution**: Upon reaching a primitive value, the function returns the resolved object (e.g., `ResolvedColor`, `ResolvedDimension`).

Successfully resolved entries update both the symbol table and their respective token maps (lines 128–176 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)), ensuring subsequent lookups return concrete values rather than reference strings.

### Phase 3: Component Property Resolution

Component definitions receive special handling during the final phase (lines 78–112 in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts)). For each property in a component definition:

- **Numbers and booleans** pass through unchanged.
- **Reference strings** trigger a call to `resolveReference` against the fully populated symbol table.
- **Successful resolutions** replace the raw reference with the resolved token object.
- **Failed resolutions** are collected in the `unresolvedRefs` array for error reporting.

This phase ensures that components receive fully materialized values ready for downstream generators like Tailwind configuration exporters.

## Configuration Limits and Safety Guards

The resolution engine enforces two critical safety limits defined 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) and loaded from [`spec-config.yaml`](https://github.com/google-labs-code/design.md/blob/main/spec-config.yaml):

- **`MAX_REFERENCE_DEPTH`** (default 10): Maximum hops allowed in a reference chain. Prevents runaway recursion when tokens reference deeply nested aliases.
- **`MAX_TOKEN_NESTING_DEPTH`** (default 20): Maximum depth of nested objects in the YAML tree. Enforced by `forEachLeaf` to maintain sanity in token structures.

When either limit is exceeded, the resolver returns `null` and generates a finding with severity `error`.

## Practical Examples

### Resolving Simple Color References

Consider a [`DESIGN.md`](https://github.com/google-labs-code/design.md/blob/main/DESIGN.md) file containing:

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

```

The resolution process executes as follows:

```typescript
// Phase 1 – Build symbol table
symbolTable.set('colors.primary', new ResolvedColor('#1A1C1E'));
symbolTable.set('colors.accent', '{colors.primary}');

// Phase 2 – Resolve chained references
const resolved = resolveReference(symbolTable, 'colors.primary', new Set(), 0);
// Returns ResolvedColor object
symbolTable.set('colors.accent', resolved);

```

Both `primary` and `accent` now point to identical `ResolvedColor` instances.

### Detecting Circular References

Circular dependencies trigger explicit error detection:

```yaml
colors:
  a: "{colors.b}"
  b: "{colors.c}"
  c: "{colors.a}"

```

The resolver walks the chain until it detects `colors.a` in the `visited` set, generating a finding:

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

```

### Resolving Component Properties

Component definitions reference tokens using the same syntax:

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

```

During component handling:

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

```

The resulting `ComponentDef` contains fully resolved objects rather than reference strings.

## Summary

- **Token reference resolution** operates through a three-phase pipeline: symbol table construction, chained reference resolution, and component property materialization.
- 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) implements recursive lookup with **cycle detection** via a `visited` Set and **depth limiting** via `MAX_REFERENCE_DEPTH`.
- **Configuration limits** defined in [`spec-config.ts`](https://github.com/google-labs-code/design.md/blob/main/spec-config.ts) prevent infinite recursion and excessively nested token structures.
- Unresolved references, circular dependencies, and depth limit violations are captured as findings in the model result for CLI reporting.

## Frequently Asked Questions

### What happens when a token reference cannot be resolved?

The `resolveReference` function returns `null` and the handler records the failure in the `findings` array with an error severity. For component properties, the raw reference string is added to `unresolvedRefs` and reported after processing completes.

### How does the resolver prevent infinite loops in circular references?

The resolver tracks visited paths using a Set passed through recursive calls. If a reference path appears in the Set during resolution, the function immediately returns `null` and the model generates a circular reference error. This detection works alongside the `MAX_REFERENCE_DEPTH` limit to prevent stack overflow.

### Can I reference composite tokens inside component properties?

According to the specification in [`docs/spec.md`](https://github.com/google-labs-code/design.md/blob/main/docs/spec.md), composite values may only be referenced within the `components` section. The resolver expects reference paths to point at primitive tokens (colors, dimensions, typography) when resolving component properties, though the symbol table itself can contain nested structures during intermediate phases.

### Where are the depth limits configured?

Depth limits are defined in [`spec-config.yaml`](https://github.com/google-labs-code/design.md/blob/main/spec-config.yaml) and exposed as constants 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 `MAX_REFERENCE_DEPTH` constant controls reference chain length, while `MAX_TOKEN_NESTING_DEPTH` controls YAML object nesting. Both values are loaded via `getSpecConfig()` at the start of the linting process.