What Causes Token Reference Resolution Errors in DESIGN.md: 7 Root Causes Explained

Token reference resolution errors occur when the DESIGN.md CLI linter cannot map a {path.to.token} reference to a concrete primitive value, caused by missing tokens, circular dependencies, excessive nesting depth beyond 50 levels, invalid syntax, or type mismatches.

Token reference resolution errors in google-labs-code/design.md occur when the CLI linter cannot reconcile the {path.to.token} syntax with a concrete primitive value in the symbol table. These failures surface during the recursive resolveReference walk in packages/cli/src/linter/model/handler.ts and are aggregated by the brokenRef rule into specific lint findings.

How the Resolution Pipeline Works

The linter processes references in three distinct phases. First, it constructs a symbol table containing every resolved token across colors, typography, spacing, and other primitive definitions. Second, it calls resolveReference recursively to walk this table and dereference each token path. Finally, the brokenRef rule in packages/cli/src/linter/linter/rules/broken-ref.ts collects any failures and emits specific findings.

An error surfaces when resolveReference returns null or when the resolved value violates the spec constraints defined in docs/spec.md at lines 86-87.

The Seven Causes of Token Reference Resolution Errors

1. Missing or Undefined Tokens

The most common error occurs when a reference points to a token that does not exist in the symbol table. In handler.ts, the resolveReference function checks symbolTable.get(path), and when this returns undefined, the function returns null immediately.

The brokenRef rule then adds this to the component's unresolvedRefs list and emits the finding: "Reference {...} does not resolve to any defined token."

components:
  button-primary:
    backgroundColor: "{colors.unknown-60}"

Running designmd lint produces:


components.button-primary → Reference {colors.unknown-60} does not resolve to any defined token.

Source: brokenRef aggregates findings from comp.unresolvedRefs at lines 25-31 in broken-ref.ts.

2. Circular Reference Chains

When tokens reference each other in a cycle, the resolver detects the loop through a visited Set. In handler.ts at lines 84-85, if visited.has(path) returns true during recursion, resolveReference returns null immediately to prevent infinite loops.

colors:
  a: "{colors.b}"
  b: "{colors.a}"

The linter reports the same "does not resolve" message because it cannot follow the chain to a concrete primitive value.

3. Exceeding Maximum Reference Depth

The resolver enforces a safety limit to prevent stack overflow. At line 83 in handler.ts, the code checks depth > MAX_REFERENCE_DEPTH (default 50) and returns null if the chain is too deep.


# 60 chained references (MAX_REFERENCE_DEPTH = 50)

colors:
  a0: "{colors.a1}"
  a1: "{colors.a2}"
  # ... a58: "{colors.a59}"

  a59: "#ff0000"

When the depth surpasses 50, the resolution aborts and triggers an unresolved-reference error.

4. Invalid Reference Syntax

Tokens must use the {path} syntax to be recognized as references. Plain values without surrounding braces are never sent to resolveReference; they are recorded as literal strings. When a component property expects a reference but receives a malformed token string, the parser flags it in brokenRef as unresolved.

5. References to Groups Instead of Primitives

The specification demands that references outside of components must resolve to primitive values (colors, dimensions, etc.), not to groups or maps. According to docs/spec.md at lines 86-87, when resolveReference returns a group object rather than a primitive, the linter flags a mismatch.

components:
  card:
    typography: "{typography}"   # `typography` is a map, not a primitive token

This violates the spec requirement for primitive resolution in most contexts.

6. Unknown Component Sub-Tokens

Components can only use whitelisted property names. The brokenRef rule checks each property against VALID_COMPONENT_SUB_TOKENS at lines 34-40 in broken-ref.ts. Unrecognized properties trigger a warning rather than a hard error.

components:
  button-primary:
    unknownProp: "{colors.primary}"

Output:


components.button-primary.unknownProp → 'unknownProp' is not a recognized component sub-token.

7. Type Mismatches in Reference Chains

Even when a reference resolves successfully, the final value undergoes type validation. If a property expects a color but the resolved chain ends with a typography object, the component-resolution loop pushes an unresolved entry. This typically produces the same "does not resolve" finding, though the underlying cause is a semantic type mismatch rather than a missing symbol.

How the Linter Aggregates Errors

The brokenRef rule serves as the central collector for all resolution failures. Located at packages/cli/src/linter/linter/rules/broken-ref.ts, this rule:

  1. Iterates through each component's unresolvedRefs list populated by failed resolveReference calls
  2. Checks property names against the VALID_COMPONENT_SUB_TOKENS whitelist
  3. Emits structured findings with file paths and line numbers

The rule distinguishes between hard errors (unresolved references) and warnings (unknown sub-tokens), though both indicate configuration issues that prevent proper token resolution.

Summary

  • Missing tokens occur when symbolTable.get(path) returns undefined in handler.ts, causing resolveReference to return null.
  • Circular references are caught by the visited Set check at lines 84-85 in handler.ts before they cause infinite recursion.
  • Depth limits trigger when reference chains exceed 50 levels, as enforced at line 83 in handler.ts.
  • Invalid syntax bypasses the resolver entirely, resulting in plaintext values where references are expected.
  • Group references fail because the spec requires primitive values outside component contexts.
  • Unknown sub-tokens are flagged by the whitelist check in broken-ref.ts at lines 34-40.
  • Type mismatches occur when resolved chains end with incompatible primitive types for the target property.

Frequently Asked Questions

How do I fix a "does not resolve to any defined token" error?

Verify that the token path inside the braces exactly matches a defined token in your symbol table. Check for typos in the path segments and ensure the referenced token is defined before it is used. If the token exists in a different file, confirm that file is being processed by the linter.

What is the maximum depth for token references in DESIGN.md?

The default maximum depth is 50 levels, defined by MAX_REFERENCE_DEPTH in packages/cli/src/linter/model/handler.ts. If your design system requires deeper nesting, you must modify this constant in the source code, though the linter prevents this to avoid stack overflow and performance degradation.

Why does my reference work in one component but fail in another?

Component properties are restricted to whitelisted sub-tokens defined in VALID_COMPONENT_SUB_TOKENS. If a property name works in one context but fails in another, check that the property is recognized by the brokenRef rule at lines 34-40 in broken-ref.ts. Additionally, ensure the reference resolves to a primitive value appropriate for that specific property type.

Can I reference a group of tokens instead of a single value?

Outside of component definitions, the specification requires references to resolve to primitive values (colors, dimensions, etc.) according to docs/spec.md at lines 86-87. Referencing an entire group or map object will trigger a resolution error unless the specific implementation explicitly supports group references for that property.

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 →