# How the DESIGN.md Linter Validates Files: A Three-Stage Pipeline

> Learn how the DESIGN.md linter validates files using a three-stage pipeline: parsing, model building, and rule execution. Discover how it catches errors and warnings in your design system.

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

---

**The DESIGN.md linter validates files through a three-stage pipeline that parses markdown and YAML front matter, builds a typed design-system model with reference resolution, and executes configurable lint rules to surface errors, warnings, and infos.**

The [`google-labs-code/design.md`](https://github.com/google-labs-code/design.md/blob/main/google-labs-code/design.md) repository provides a robust validation system for design token files. Understanding how the DESIGN.md linter validates files helps you debug configuration issues and extend the tool with custom rules. The entire process is orchestrated by the `lint()` function in [`packages/cli/src/linter/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/lint.ts) and operates without side effects, returning a pure `LintResult` that the CLI formats as JSON or plain text.

## Stage 1: Parsing the Markdown

The validation process begins with **ParserHandler**, located in [`packages/cli/src/linter/parser/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/parser/handler.ts). This stage walks the AST created by **unified** and **remark-parse** to extract structured data from the markdown.

The parser handles two critical tasks:

- **YAML extraction** – It extracts YAML front matter or fenced `yaml` code blocks and parses them using the **yaml** library. All YAML blocks are merged into a single token map.
- **Source mapping** – It records line numbers for every token to provide accurate error locations later.

If the parser detects duplicate top-level sections, it reports them as recoverable errors rather than throwing exceptions. The output includes the raw sections, document sections, and the source map for downstream consumption.

## Stage 2: Building the Design-System Model

Next, **ModelHandler** (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)) receives the parsed token map and constructs a validated `DesignSystemState`. This stage transforms raw YAML tokens into a typed design system.

Key responsibilities include:

- **Type validation** – It validates primitive token types including colors, dimensions, and typography values.
- **Reference resolution** – It resolves token references like `{color: "{primary}"}` through a symbol table, catching circular dependencies and undefined tokens.
- **Component building** – It constructs component definitions and validates their integrity.
- **Error collection** – Invalid values (malformed colors, unsupported units, excessive nesting depth) become `Finding` objects with appropriate severity. The model never throws; all problems are collected as findings.

The model also flags unknown top-level keys, ensuring the design system adheres to the expected schema before rule evaluation begins.

## Stage 3: Running Lint Rules

The final stage executes the `runLinter` function defined in [`packages/cli/src/linter/linter/runner.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/runner.ts). This stage operates on the resolved `DesignSystemState` and applies the **DEFAULT_RULES** set.

Rule execution is pure and side-effect free:

- Each rule receives the `DesignSystemState` and may emit `Finding` objects classified as error, warning, or info.
- The runner aggregates findings from all rules and builds a summary containing counts of errors, warnings, and infos.
- Individual rules live in `packages/cli/src/linter/linter/rules/*` (e.g., [`missing-primary.ts`](https://github.com/google-labs-code/design.md/blob/main/missing-primary.ts), [`contrast-ratio.ts`](https://github.com/google-labs-code/design.md/blob/main/contrast-ratio.ts)).

The runner returns a `LintResult` containing the aggregated findings and summary statistics, which the CLI command in [`packages/cli/src/commands/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/commands/lint.ts) formats for display.

## Entry Point and Orchestration

The top-level `lint()` function in [`packages/cli/src/linter/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/lint.ts) orchestrates the three-stage pipeline:

```ts
import { ParserHandler } from './parser/handler.js';
import { ModelHandler } from './model/handler.js';
import { TailwindEmitterHandler } from './tailwind/handler.js';
import { runLinter } from './linter/runner.js';

const parser = new ParserHandler();
const model = new ModelHandler();
const tailwind = new TailwindEmitterHandler();

const parseResult = parser.execute({ content });

if (!parseResult.success) {
  // Recoverable parse error becomes a warning
}

const { designSystem, findings: modelFindings } = model.execute(parseResult.data);
const lintResult = runLinter(designSystem, options?.rules);

```

The combined findings from the model validation and rule execution are returned to the caller, along with a generated Tailwind configuration if applicable.

## Usage Examples

### Running the Linter from the CLI

Validate a DESIGN.md file and output JSON formatted results:

```bash

# Validate a specific file

design-md lint path/to/DESIGN.md --format json

# Validate from stdin

cat DESIGN.md | design-md lint - --format json

```

### Programmatic Usage

Import the `lint` function to validate content directly in TypeScript:

```ts
import { lint } from 'design-md/packages/cli/src/linter/lint.js';
import { readFileSync } from 'fs';

const content = readFileSync('examples/totality-festival/DESIGN.md', 'utf8');
const report = lint(content);

// Access validation results
console.log(report.summary);
// { errors: 0, warnings: 2, infos: 1 }

// Iterate over specific findings
report.findings.forEach(finding => {
  console.log(`${finding.severity}: ${finding.message} at ${finding.path}`);
});

```

The report object contains:
- **designSystem**: The fully resolved model
- **findings**: Array of `{severity, path?, message}` objects
- **summary**: Aggregated counts of errors, warnings, and infos
- **tailwindConfig**: Generated Tailwind theme configuration

### Custom Rule Sets

Extend the linter with custom rules by passing a `LintOptions` configuration:

```ts
import { lint, LintOptions } from 'design-md/packages/cli/src/linter/lint.js';
import { myCustomRule } from './my-rules.js';

const opts: LintOptions = { 
  rules: [myCustomRule] 
};

const report = lint(myDesignMdContent, opts);

```

Custom rules receive the `DesignSystemState` and return `Finding` objects, behaving identically to the default rules in `packages/cli/src/linter/linter/rules/*`.

## Summary

- **ParserHandler** extracts YAML and markdown structure from [`packages/cli/src/linter/parser/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/parser/handler.ts), creating a source-mapped token map.
- **ModelHandler** validates token types and resolves references 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), producing a `DesignSystemState` or `Finding` objects for errors.
- **runLinter** executes the rule set in [`packages/cli/src/linter/linter/runner.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/linter/runner.ts), aggregating all findings into a `LintResult`.
- The **lint()** entry point in [`packages/cli/src/linter/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/lint.ts) orchestrates the pipeline and returns both the validated model and Tailwind configuration.
- All stages are pure functions that return findings rather than throwing, enabling programmatic error recovery and custom reporting.

## Frequently Asked Questions

### What happens if the YAML front matter contains syntax errors?

The **ParserHandler** catches YAML syntax errors during the parsing stage and reports them as recoverable findings with `severity: error`. The linter continues processing to detect additional issues, returning all findings in the final report rather than failing on the first error.

### How does the linter resolve token references like `{color: "{primary}"}`?

During the model building stage, **ModelHandler** maintains a symbol table of all defined tokens. When it encounters a reference like `"{primary}"`, it resolves the value through this table, checking for circular dependencies and undefined tokens. Unresolvable references generate `Finding` objects that identify the specific path and missing token name.

### Can I disable specific lint rules or add custom ones?

Yes. The `lint()` function accepts an optional `rules` array in `LintOptions`. You can pass a subset of the **DEFAULT_RULES** to disable specific checks, or provide custom rule functions that receive the `DesignSystemState` and return `Finding` arrays. Custom rules reside alongside the defaults in `packages/cli/src/linter/linter/rules/*` or in your own source files.

### Where does the linter store information about line numbers for error reporting?

The **ParserHandler** creates a *source map* during the initial parsing stage, recording line numbers for every extracted token. This source map travels with the data through the model and rule stages, ensuring that `Finding` objects reference the original line numbers in [`DESIGN.md`](https://github.com/google-labs-code/design.md/blob/main/DESIGN.md) even after the YAML blocks have been parsed and merged.